| description | cdkd's directory layout and per-file purpose notes (Core Directories, Important Files, SDK Providers) | |
|---|---|---|
| paths |
|
-
src/cli/ - CLI command implementations (deploy, destroy, diff, drift, events, gc, synth, list/ls, bootstrap, force-unlock, import, export, publish-assets, state, local), config resolution.
Top-level vs
statesubcommand split: top-level commands (deploy,destroy,diff,synth,list,import,orphan) require a CDK app — they synthesize a template to know what they're operating on. Thecdkd state ...subcommand family (state info,state list,state resources,state show,state orphan,state destroy,state migrate) operates on the S3 state bucket only and does NOT need the CDK code; it's the right place to inspect / clean up state when the CDK app is missing or you don't want to synth.cdkd drift <stack>is also state-driven (no synth), since it compares state-recorded properties to the AWS-current snapshot returned by each provider's optionalreadCurrentStatemethod — a CC-API fallback covers the majority of resource types out of the box; SDK Providers add their ownreadCurrentStateincrementally. The twoorphancommands operate at different granularities (this is the breaking change in PR #92):cdkd orphan <constructPath>...is per-resource (mirrors upstreamcdk orphan --unstable=orphan) and rewrites every sibling reference (Ref / Fn::GetAtt / Fn::Sub / dependencies) so the next deploy doesn't re-create the orphan;cdkd state orphan <stack>...is whole-stack and removes the entire state record without touching siblings. Both orphan variants delete ONLY cdkd state; AWS resources are left intact (usedestroy/state destroyto delete them).cdkd import <stack> --app "..."adopts AWS-deployed resources into cdkd state. Three modes: (1) auto (no flags) — every resource in the template is resolved from its physical-name property, then from a same-named CloudFormation stack'sDescribeStackResources(issue #1128 / #1130; there is deliberately noaws:cdk:pathtag lookup — that tag can never exist on a real resource, issue #1134); (2) selective (CDK CLI parity, default whenever--resource <logicalId>=<physicalId>,--resource-mapping <file.json>, or--resource-mapping-inline '<json>'is supplied) — ONLY the listed resources are imported, every other template resource is reported asout of scopeand left out of state for the next deploy to CREATE. Matchescdk import --resource-mapping/--resource-mapping-inlinesemantics, including refusing to silently no-op on a typo'd logical ID;--resource-mappingand--resource-mapping-inlineare mutually exclusive (matches upstream); (3) hybrid (--autowith overrides) — listed resources use the explicit physical id; the rest still go through the same name / CloudFormation auto-resolution as mode (1).--record-resource-mapping <file>writes cdkd's resolved{logicalId: physicalId}map (covers explicit overrides AND auto / hybrid mode tag-lookups) to disk before the confirmation prompt — emitted even when the user says "no" or under--dry-run, so the resolved data can be replayed as--resource-mappingin non-interactive CI re-runs (mirrorscdk import --record-resource-mapping). Existing-state semantics: selective mode is non-destructive — listed resources are merged into the existing state file and unlisted entries are preserved.--forceis required only when the import would lose data: auto / whole-stack mode against existing state (rebuilds the resource map from the template, dropping any state entry not re-imported), or selective mode where a listed override would overwrite a resource already in state. First-time imports against an empty state never need--force. Outputs in the existing state are inherited by both modes (the import flow never derives outputs).--migrate-from-cloudformation [cfn-stack-name](cdkd-specific) extends the import flow with an end-to-end migration path off CloudFormation. The flow: (1) before the import loop,getCloudFormationResourceMapping(...)(insrc/cli/commands/retire-cfn-stack.ts) issues a singleDescribeStackResourcesagainst the named CFn stack and merges the resultingMap<logicalId, physicalId>into the import overrides (user-supplied--resource/--resource-mappingentries take precedence). This side-steps cdkd's tag-based auto-lookup — which can't find resources deployed by upstreamcdk deploy(that flow doesn't propagateMetadata.aws:cdk:pathas an AWS tag, and AWS reserves theaws:tag prefix so cdkd can't add it on the way through either — confirmed empirically in #1128:TagRolewith anaws:-prefixed key returnsInvalidInput: Tag keys beginning with aws: are reserved for system use, and acdk deploy-deployed resource carries tags[]while its template Metadata holds the path. The corollary is that theaws:cdk:pathtag walk NEVER matches. Auto-mode import therefore resolves ids from the template's physical-name property and — since #1128 — from a same-named CloudFormation stack'sDescribeStackResources, via the best-effort, never-fataltryGetCloudFormationResourceMap. That lookup is flat (nested children stay--migrate-from-cloudformation's job), never retires the source stack, is skipped in selective mode, and warns-and-falls-through on any failure so a caller lackingcloudformation:DescribeStackResourcesis not blocked) — so a barecdkd import MyStack --migrate-from-cloudformationworks for bothcdk deploy-managed andcdkd deploy-managed stacks. The flag also forcesselectiveMode = falseregardless of override count (the CFn-derived overrides shouldn't trigger selective mode, which would mark every other template resourceout of scopeand orphan them afterDeleteStack). (2) Import runs and writes state. (3) After state write,retireCloudFormationStack(...)runs the standardDescribeStacks(verify stable terminal state, capture existingCapabilities) →GetTemplateOriginal-stage (parse JSON, injectDeletionPolicy: Retain+UpdateReplacePolicy: Retainon every resource) →UpdateStack(skipped when the diff is empty or every resource already has both Retain policies) →DeleteStack(CFn skips every resource because they're now Retain). Runs inside the import command's lock-protected scope so a concurrentcdkd deploycan't race the post-write CFn calls; only runs when state was actually written (zero-imports or "no" at the prompt skip the retirement). The flag accepts an optional value: bare--migrate-from-cloudformationuses the cdkd stack name as the CFn stack name (typical for CDK apps where they match); pass--migrate-from-cloudformation <name>to override when the names differ. Templates may be JSON or YAML (CFn shorthand intrinsics like!Ref/!GetAtt/!Subare preserved across parse → mutate → re-serialize via the CFn-aware codec at src/cli/yaml-cfn.ts — see the YAML support bullet for details). Templates up to the 51,200-byte inlineTemplateBodyceiling are submitted directly; larger templates are uploaded to the cdkd state bucket undercdkd-migrate-tmp/<stack>/<timestamp>.{json,yaml}and submitted viaTemplateURL(the transient object is deleted in afinallyimmediately afterUpdateStack, success or failure). Templates over the 1 MB CloudFormationTemplateURLceiling are structurally unsubmittable and fail with a clear error; cdkd state is already written so the user can re-run or finish manually. Not compatible with--dry-run(post-state-write retirement is a real side-effect). For plain (non-CDK) CloudFormation stacks (hand-authored YAML / JSON, Terraform-to-CFn output, Console-created stacks) use the dedicatedcdkd migrate --from-cfn-stack <name>top-level command, which wraps the same end-to-end flow (upstreamcdk migratecodegen + 2-pass(sourceLogicalId, synthLogicalId)mapping + cdkd state + optional--retire-cfn-stack).cdkd import --migrate-from-cloudformationis the right tool when a CDK app already exists and you want to take over an existingcdk deploy-managed CFn stack without re-generating the CDK code.provider.importsupport coverage: see docs/import.md for the full per-resource-type list (auto-lookup vs override-only vs CC-API fallback vs unsupported). Single source of truth — when addingimport()support to a provider, update that file. Keep entries one-per-line so parallel PRs don't conflict on rebase.cdkd importvs upstreamcdk import— parity notes (see docs/import.md for the full matrix; this is a quick checklist when working on the import code path):- Mechanism is per-resource SDK calls, not a CloudFormation changeset.
cdkd importis therefore not atomic.import.tscollects per-resource outcomes (imported/skipped-not-found/skipped-no-impl/skipped-out-of-scope/failed) and only writes state after a final confirmation (--yesto skip). A partial import can be backed out withcdkd state orphan <stack>. - No interactive prompt for missing IDs. Upstream's TTY default prompts per resource; cdkd resolves IDs from the template's physical-name property + a same-named CloudFormation stack's
DescribeStackResources(inauto/hybridmodes) or treats them asout of scope(in selective mode). The only prompt is the final "write state?" gate. --resource-mapping <file>: parity. Same JSON shape ({"LogicalId": "physical-id"}) and same semantics — only listed resources imported, unlisted resources rejected, typo'd logical IDs abort before any AWS call.--resource-mapping-inline '<json>': parity. Same JSON shape as--resource-mapping <file>, mutually exclusive with it. Useful in non-TTY CI scripts that don't want a separate file.--record-resource-mapping <file>: parity. cdkd writes the resolved{logicalId: physicalId}map to the file before the confirmation prompt (and even when the user says "no" or under--dry-run). Covers explicit overrides AND cdkd's tag-based auto-lookup, so this is the canonical way to capture anauto-mode resolution and replay it as--resource-mappingin CI.--forcesemantics differ. Upstream: "continue even if the diff has updates/deletions." cdkd: "confirm a destructive write to existing state" — required for auto / whole-stack rebuild on existing state, and for overwriting a listed entry already in state during selective mode; not required for a pure selective merge that only adds new resources, nor for first-time imports. Same flag name, different meaning — do not confuse them when reading PRs / issues.autoandhybridmodes are cdkd-specific (whole-stack adoption via physical-name + CloudFormationDescribeStackResourcesresolution; noaws:cdk:pathtag lookup — issue #1134). No upstream equivalent. Do not mistake them for parity features.--migrate-from-cloudformation [name]is cdkd-specific. End-to-end migration off CloudFormation: pre-importDescribeStackResourcesto recover physical IDs (so cdk-deployed stacks work without--resource) → import → state write → post-importUpdateStack(inject Retain; uploaded to the cdkd state bucket viaTemplateURLwhen over the 51,200-byte inline limit, hard-rejected over the 1 MBTemplateURLceiling) →DeleteStack. No upstream equivalent —cdk importonly adopts resources INTO a CFn stack, never out of one. Accepts JSON and YAML templates (CFn shorthand intrinsics preserved end-to-end via the codec at src/cli/yaml-cfn.ts); incompatible with--dry-run(see the import section above for the full constraint list).- Nested CloudFormation stacks (
AWS::CloudFormation::Stack): barecdkd import(auto / selective / hybrid mode) reports each nested-stack row asunsupported(NestedStackProviderhas noimport()).cdkd import --migrate-from-cloudformationIS supported recursively (issue #464): it walksDescribeStackResourcesrecursively, writes one v6-keyed state file per nested child (cdkd/<parent>~<childLogicalId>/<region>/state.json) withparentStack/parentLogicalId/parentRegionpopulated, recursively injectsDeletionPolicy: Retainon every leaf resource (parent + every child template), and retires the whole tree via a single parent-sideDeleteStackcascade. Children at every level are fetched in parallel. Per-child locks are acquired before each child write and released in reverse on success or failure. The root parent's state entry for each nested-stack row carries the synthesized cdkd-local ARN (matching whatNestedStackProvider.createwrites at deploy time — NOT the real AWS child stack ARN). CDK Stages (separate top-level stacks under one app) work fine. - No CDK bootstrap version requirement. cdkd uses its own S3 state bucket; the upstream "bootstrap v12+" caveat does not apply.
cdkd export <stack>is the mirror ofcdkd importin the reverse direction (cdkd → CloudFormation). It synthesizes the CDK app to get the template, reads cdkd state for(logicalId, physicalId)mappings, refuses if any template resource is in the never-importable set (Custom::*ANDAWS::CloudFormation::CustomResource— the type CDK emits fornew cdk.CustomResource(...)withoutresourceType; both are Lambda-backed Custom Resources that CFn cannot adopt) or has no entry in cdkd state.AWS::CloudFormation::Stackrows are fully supported as of issue #464 PR B2:buildImportPlanroutes each row into a dedicatednestedStackRows: NestedStackRow[]list, the orchestrator invokesbuildCdkdStateStackTree(rootStackName, region, stateBackend)to recursively load every child state file fromcdkd/<parent>~<childLogicalId>/<region>/state.json(failing fast on a torn tree), andrunPerStackImportLoopsubmits one CFn IMPORT changeset per cdkd-managed stack in the tree in leaf-first order. Non-leaf parents adopt their just-imported children as nested references via the AWS-docs "Nest an existing stack" pattern (DeletionPolicy: RetainplusResourceIdentifier: { StackId: <child arn> }plus aTemplateURLrewritten to point at the child's AWS-canonicalized template fetched viaGetTemplate(Processed)post-IMPORT). Each child cdkd stack<parent>~<childLogicalId>becomes its own CFn stack named<parent>-<childLogicalId>by default viacdkd2cfnStackName(CFn rejects~in stack names); per-child overrides via--cfn-child-stack-name '<cdkdName>=<cfnName>'(repeatable). Per-child Parameters are extracted from the parent template'sAWS::CloudFormation::Stack.Properties.Parametersblock byextractChildImportParameters(literal string / number / boolean classification) and then intrinsic-resolved byresolveChildImportParameters— a root-first pre-pass (buildResolvedParametersPerStack, walkingflattenCdkdStateTreeRootFirst) runs the deploy engine'sIntrinsicFunctionResolverso{Ref: <ParentParam>}/{Fn::GetAtt: [ParentResource, Attr]}resolve against the parent's already-resolved Parameters + cdkd state BEFORE each child's standalone IMPORT (CFn's atomic nested create did this implicitly; cdkd's per-stack leaf-first loop must do it explicitly — which is why resolution is root-first while submission is leaf-first). Values the resolver cannot handle degrade to alogger.warn+ the child template'sDefault(the pre-resolver behavior), so adding resolution never regresses a working export. The original "one atomic--include-nested-stacksIMPORT changeset" design was found infeasible by the 2026-05-24 AWS spike (AWS rejects that flag combination withValidationError: IncludeNestedStacks is not supported for changeSet type: IMPORT) — see design §4.0/§4.3 for the per-stack-loop redesign. The command resolves each non-nested-stack resource type's primary identifier property viacloudformation:DescribeType(with a hardcoded fallback table insrc/cli/commands/export.tsfor ~30 common types — covering S3 / IAM / Lambda / DynamoDB / SQS / SNS / Logs / EC2 / RDS / Events / API Gateway etc.), acquires the stack lock, confirms with the user, preprocesses the phase-1 template (strip Outputs; injectDeletionPolicy: Deleteon resources missing the attribute — matches the CFn type-default for resources without explicitRemovalPolicy; overlay each resource'sResourceIdentifieronto itsPropertiesso CFn IMPORT's identifier-match check passes against cdkd's stack-name-prefixed physical ids), then issuesCreateChangeSet --change-set-type IMPORT→ wait →ExecuteChangeSet→waitUntilStackImportComplete, and finally deletes cdkd state for the migrated stack. AWS resources are unchanged across the migration; the stack is then managed bycdk deploy/aws cloudformation. Context preservation guard: refuses by default if CLI-c key=valueoverrides are supplied, because those values are not persisted tocdk.json/cdk.context.jsonand a subsequentcdk deploywithout the same-cflags would synthesize a different template (drift / replacement on first post-migration deploy). User moves the values tocdk.json'scontext: {}field (recommended) or passes--accept-transient-contextto opt in to the risk. On success, prints the exactcdk diff/cdk deploycommand including any captured-cflags. MVP scope: JSON and YAML templates supported (via the CFn-aware codec at src/cli/yaml-cfn.ts — see the YAML support bullet for details); all-or-nothing (if any resource cannot be imported, the whole command aborts — destroy or accept abandoning those resources first), inlineTemplateBodyonly (51,200-byte cap), synth template used verbatim (noobservedPropertiessubstitution). Caveats: (1) replacement risk on nextcdk deployif the CDK code does not specify explicit physical names (bucketName: 'my-bucket-12345') — same long-standing UX as upstreamcdk import; users should set explicit names before exporting or inspect the post-import changeset before executing. (2) cross-stackFn::GetStackOutputconsumers in other cdkd stacks cannot read the exported stack's outputs anymore (CFn outputs live in CFn, cdkd's resolver reads cdkd state); plan multi-stack migrations from the leaves up. Implementation insrc/cli/commands/export.ts.stateis a parent command for inspecting and manipulating cdkd's S3 state bucket:state infoprints bucket name, region (auto-detected viaGetBucketLocation), the source that resolved the bucket (cli-flag/env/cdk.json/default/default-legacy), the schema version, and a stack count (with--jsonfor tooling);state list(aliasls) lists deployed stacks (one row per(stackName, region)pair under the new region-prefixed key layout;--tree(issue #555 A3) loads each state record to read the v6parentStack/parentRegionfields and renders atree(1)-style parent → child hierarchy via src/cli/commands/state-list-tree.ts — flat default is preserved for backward compatibility with scripts that grep the existing one-row-per-stack output, and--tree --jsonemits the nested JSON shape for tooling; orphan children whose parent record is missing surface at root level rather than vanishing);state resources <stack>andstate show <stack>accept--stack-region <region>to disambiguate when the same stackName has state in multiple regions;state show <stack> --show-nested(issue #555 A4) reusesbuildCdkdStateStackTree(fromsrc/cli/commands/export.ts) to recursively walk everyAWS::CloudFormation::Stackrow in the target's state and append each child's full state block after the parent's (DFS order, flat at column 0 withNested stack: <name>headers;--show-nested --jsonemits the recursive{state, lock, children: [...]}shape withchildrenalways present even on leaves so consumers see a stable key set); default (no--show-nested) preserves the existing single-stack{state, lock}JSON shape verbatim for backward compatibility with tooling consumers;state orphan <stack>...removes cdkd's state record for every region by default, or scopes to one with--stack-region <region>(does NOT delete AWS resources — name mirrors aws-cdk-cli's newcdk orphan);cdkd orphan <constructPath>...is the synth-driven, per-resource counterpart (mirrors upstreamcdk orphan --unstable=orphan) — it removes specific resources from a stack's state file by construct path (MyStack/MyTable), live-fetching everyFn::GetAttit has to substitute via the resource'sprovider.getAttribute()(cached per(orphan, attr)) and rewriting every siblingRef/Fn::GetAtt/Fn::Sub/dependenciesreference so the next deploy doesn't try to re-create the orphan or fail on a stale reference. Path matching is prefix-based (matches upstream's behavior): the user's input matches every resource whoseaws:cdk:pathis exactly the input OR starts with<input>/, so an L2 path likeMyStack/MyConstruct/MyBucketresolves to the synthesized L1 childMyStack/MyConstruct/MyBucket/Resource, and an L2 wrapper that contains multiple CFn resources orphans every child under it. Theaws:cdk:pathindex insrc/cli/cdk-path.tsexcludesAWS::CDK::Metadataresources so the synthesized<Stack>/CDKMetadata/Defaultsentinel is never offered as an "available path" and cannot be orphaned; unresolvable references hard-fail with a one-shot list of every site, and--forcefalls back to the orphan'sstate.attributescache (logging a per-case warning) before leaving the original intrinsic untouched if the cache also lacks the attr;--dry-runprints the rewrite audit table without acquiring a lock or saving state. The implementation lives insrc/analyzer/orphan-rewriter.ts(the recursion structure mirrorsIntrinsicFunctionResolverbut in the inverse direction: only orphan references are substituted, every other intrinsic is left alone) andsrc/cli/cdk-path.ts(the sharedaws:cdk:pathindex, also used bycdkd import). The pre-PRcdkd orphan <stack>whole-stack behavior is gone — the command hard-fails with a redirect message that points tocdkd state orphan <stack>instead of silently routing.state destroy <stack>...deletes AWS resources AND the state record without requiring the CDK app (the CDK-app-free counterpart tocdkd destroy). The per-stack destroy logic is hoisted intosrc/cli/commands/destroy-runner.tsand shared by bothcdkd destroyandcdkd state destroy. As of #555 A2,state destroyis ALSO the documented escape hatch for directly destroying a nested-stack child —cdkd destroy <child>is refused withNestedStackChildDirectDestroyError(matches CFn's "you can't directly destroy a nested stack" semantic; the parent'sAWS::CloudFormation::Stackrow would otherwise point at gone-from-AWS resources and the parent's next deploy would try to recreate them), butcdkd state destroy <child>intentionally bypasses the guard for users who accept leaving the parent's reference dangling.state migratecopies all state from the legacy region-suffixed default bucket (cdkd-state-{accountId}-{region}) to the new region-free default (cdkd-state-{accountId}); refuses to run while any stack has an active lock; verifies object-count parity before any source cleanup; source bucket is kept by default and only deleted with--remove-legacy. The bucket-name banner is no longer printed in routine command output (it includes the AWS account id, which would leak via screenshots / public CI logs); pass--verboseto surface it in debug logs, or usestate infofor an explicit on-demand answer. - Mechanism is per-resource SDK calls, not a CloudFormation changeset.
-
src/synthesis/ - CDK app synthesis (self-implemented: subprocess execution, Cloud Assembly parsing, context providers)
-
src/analyzer/ - DAG builder, template parser, intrinsic function resolution
-
src/state/ - S3 state backend, lock manager
-
src/deployment/ - DeployEngine (orchestration), WorkGraph (DAG-based asset+deploy scheduling)
-
src/provisioning/ - Provider registry, Cloud Control provider, SDK providers
-
src/assets/ - Asset publisher (self-implemented S3 file upload with ZIP packaging, ECR Docker image build & push)
-
src/local/ -
cdkd local invoke,cdkd local start-api,cdkd local run-task,cdkd local start-service,cdkd local start-alb,cdkd local start-cloudfront,cdkd local invoke-agentcore, andcdkd local start-agentcorebuilding blocks (renamed fromsrc/local-invoke/to share the directory with the rest of thecdkd localfamily — see PR #228). Thestart-cloudfront+start-agentcorecommands are THIN factory pass-throughs whose command files live atsrc/cli/commands/local-start-cloudfront.ts/local-start-agentcore.ts— each wraps a cdk-localcreateLocalStart*Commandfactory, re-hands the active embed config, AND threads cdkd's--from-statefactory through the factory'sextraStateProvidersseam (issue #766; thestart-agentcorefactory carried the seam from the start, thestart-cloudfrontfactory gained it in cdk-local 0.128.0 / cdk-local#426). Both layer the cdkd-specific--from-state/--state-bucket/--state-prefixflags on top of cdk-local's inherited--from-cfn-stack/--stack-region(cdk-local#380 also gavestart-cloudfrontLambda Function URL + deployed-S3 origins). The ECS run-task family addsecs-task-resolver.ts(synth template →ResolvedEcsTaskwith containers / volumes / DependsOn / RuntimePlatform),ecs-secrets-resolver.ts(Secrets[].ValueFrom→ real values via SecretsManager / SSM),ecs-network.ts(per-task docker network + AWS-published metadata-endpoints sidecar lifecycle), andecs-task-runner.ts(top-level orchestrator: image prep → DAG topo-sort → docker run loop → log stream → teardown). The ECS start-service family (#466, #460) originally addedecs-service-resolver.ts+ecs-service-runner.ts+ Cloud Mapcloud-map-registry.ts/cloud-map-resolver.tsmodules carrying the per-replica orchestrator + Service Connect / Cloud Map DNS-only overlay; the Part B refactor (PR #731, 2026-05-30) moved every one of those modules to cdk-local's bundledrunEcsServiceEmulatorengine + deleted them from cdkd's tree. The per-CLI-run shared docker network (cdkd-local-svc-<rand>, subnet169.254.171.0/24, sidecar at169.254.171.2) and the Cloud Map peer-discovery overlay are now engine-owned. Seedocs/changelog-cdkd.md's Part B + Part A entries for the historical detail, and the bottom of this bullet for the current shim wiring.cdkd local invokemodules:lambda-resolver.ts(target → discriminatedResolvedLambda(kind: 'zip' | 'image') carrying StackInfo / logicalId / runtime+handler+codePath for ZIP or imageUri+imageConfig for IMAGE; both variants carryarchitecture(issue #768) so the ZIP container run pins--platformthe same way the IMAGE path always has; reusescdk-path.tsandstack-matcher.ts),env-resolver.ts(template literals + SAM-shape--env-varsoverrides; intrinsic-valued entries warn-and-drop unless--from-statesubstituted them upstream),state-resolver.ts(PR 2 — pure-functional substituter that walks intrinsic-valued env-var values againststate.resourcesfrom cdkd's S3 state file; supportsRef/Fn::GetAtt/Fn::Sub, reports per-key unresolved reasons),runtime-image.ts(Runtime→public.ecr.aws/lambda/<lang>:<v>+ source-file extension; v1 supportsnodejs18.x/nodejs20.x/nodejs22.x/python3.11/python3.12/python3.13),docker-runner.ts(thinexecFile/spawnwrappers arounddocker pull/docker run -d --rm --name <optional>/docker logs -f/docker rm -f+ free-port allocator; PR 5 extendedrunDetachedwith--platform/--entrypoint/--workdir; PR 8a added the optional--namefor orphan-sweep),docker-image-builder.ts(PR 5 — local-build path for container Lambdas, wraps the sharedsrc/assets/docker-build.tshelper with a stable per-context tag),ecr-puller.ts(PR 5 — ECR-pull fallback when the cdk.out asset lookup misses; same-account / same-region only, cross-acct/region hard-errors with a deferred-PR pointer), andrie-client.ts(HTTPPOST /2015-03-31/functions/function/invocationsto RIE inside the container, plus a TCP-probe-based readiness wait).cdkd local start-apimodules (PR 8a):route-discovery.ts(REST v1 + HTTP API + Function URL →DiscoveredRoute[]with a 30-line local intrinsic resolver — no deploy-state dependency),api-gateway-event.ts(pure-functional v1 + v2 event-shape builders + PR 8bapplyAuthorizerOverlay),api-gateway-response.ts(Lambda response → HTTP, with auto-format / error-envelope / cookies-as-multiple-Set-Cookie translation),route-matcher.ts(3-tier precedence: full → greedy{proxy+}→$default, with literal-segment tie-break),container-pool.ts(per-Lambda warm container pool with mutex-protected lazy growth, 60s idle GC, dispose-tolerates-removeContainer-failures), andhttp-server.ts(thenode:httpaccept loop with PR 8b authorizer pass and PR 8c's atomicsetServerStateswap for hot reload). PR 8b additions:authorizer-resolver.ts(REST v1 / HTTP v2 / Function URL authorizer detection + identity-source parsing — extended in #447 with theIamAuthorizerdiscriminated union member for REST v1AuthorizationType: 'AWS_IAM', and again in #621 wiring Function URLAuthType: 'AWS_IAM'through the same descriptor so it rides the same SigV4 verifier; #470 added support forFn::GetAtt: [<UserPool>, 'Arn']underProviderARNs[]— the canonical CDKapigateway.CognitoUserPoolsAuthorizershape — by synthesizing an unreachable placeholder ARN socognito-jwt.ts's JWKS pass-through fallback admits every token without signature verification),authorizer-cache.ts(TTL-aware result cache),lambda-authorizer.ts(TOKEN + REQUEST authorizer invoke + IAM-policy parser),cognito-jwt.ts(JWKS fetch + RS256 verify + claims extraction + pass-through fallback),sigv4-verify.ts(#447 — REST v1 AWS_IAM SigV4 signature verification against the dev's local credentials viaSTSClient's default credential chain; signature verification only, no IAM policy emulation; warn-and-pass on foreign-identity requests perfeedback_match_aws_default_over_opinionated.md). PR 8c additions:cors-handler.ts(CFnCorsConfigurationparser + OPTIONS preflight matcher for HTTP API v2),stage-resolver.ts(per-API Stage selection +attachStageContextfor routes; populatesevent.stageVariables),file-watcher.ts(chokidar-backed debounced file watcher with dynamic path-list updates),reload-orchestrator.ts(synth-failure-tolerant reload pipeline with chain-serialized concurrent calls).invoke-agentcore-watch-loop.ts(#270) is the cdkd-ownedcdkd local invoke-agentcore --watchreload loop — built on cdk-local's exported watch primitives (createFileWatcher/createWatchPredicates/resolveWatchConfig/classifySourceChange, the same oneslocal start-api --watchuses) plus copies of cdk-local's not-exportedloadAgentCoreAssetContext/deriveOldAssetHashhelpers; it takesrebuild/softReloadcallbacks from the command so the per-firing classifier picks adocker cp+restart soft-reload (interpreted-handler source edit) vs a full image rebuild (Dockerfile / compiled / asset-hash change), re-opening the/wssocket or re-running the one-shot/invocationson each reload. cdk-local's ownrunAgentCoreWatchLoopcould not be shimmed because it hard-couples to cdk-local'sSynthesizer/LocalInvokeAgentCoreOptionstypes.intrinsic-image.ts(issue #286 Gap 2) holds the shared canonical-CDK-2.x-Fn::Join-shape resolver for container image URIs (lambda.DockerImageCode.fromEcr+ ECSContainerImage.fromEcrRepository) —tryResolveImageFnJoin+substituteImagePlaceholders+ theImageResolutionContext/FnJoinResolveOutcometypes, used by bothlambda-resolver.tsandecs-task-resolver.ts.intrinsic-lambda-arn.ts(issue #286 Gaps 3 / 4) is the sibling helper for Lambda ARN intrinsics in API Gateway resolvers —resolveLambdaArnIntrinsicacceptsRef/Fn::GetAtt: [..., 'Arn']/ the REST v1 invoke-ARNFn::Joinwrapper (also emitted by CDK 2.x's HTTP v2HttpLambdaAuthorizer) / theFn::Subinvoke-ARN wrapper (both 1-arg${LogicalId.Arn}form and 2-argFn.sub(template, vars)form). Returns a discriminated union so each call site (route-discovery.tsforIntegrationUri,authorizer-resolver.tsforAuthorizerUri) wraps the unsupported case with its own error class.intrinsic-utils.ts(#471) holds the sharedpickRefLogicalIdhelper — extracts the referenced logical ID from a{Ref: <string>}intrinsic, returnsnullotherwise. Consumed byroute-discovery.ts,websocket-route-discovery.ts,authorizer-resolver.ts, andstage-resolver.ts. Centralizes a 5-line predicate that was previously duplicated four times so future intrinsic-shape extensions (e.g. acceptingFn::Sub-bound Refs in REST v1 ResourceId / ParentId) land in one place.authorizer-context.ts(PR #515 item 9) is the per-kind shape builder consumed today byhttp-server.ts'sbuildAuthorizerContextForServiceIntegration(HTTP API v2 service-integration$context.authorizer.*parameter-mapping context). Owns the bare per-kind shape (Lambda flatprincipalId + context, IAMprincipalIdonly, Cognito{claims}, JWT{jwt: {claims, scopes}}). The siblingbuildOverlayinhttp-server.ts(Lambda AWS_PROXY event overlay) still uses hand-rolled per-kind branching because it wraps the result in theAuthorizerEventOverlaydiscriminated union shape (with thelambda-http-v2arm layering an additional.lambdanamespace); the inner per-kind context matches the helper's output exactly, so a future kind addition can be lifted through this helper at both call sites with no behavior change. #457 additions:vtl-engine.tsis a hand-rolled minimal AWS API Gateway VTL evaluator ($input/$context/$utilbuilt-ins,#set/#if/#elseif/#else/#foreach/##directives, JSONPath subset — no external dep) used by every REST v1 non-AWS_PROXY dispatcher;integration-response-selector.tsresolvesIntegrationResponses[].SelectionPattern(regex anchored^...$) +ResponseParametersheader literals +ResponseTemplatesAccept-header content negotiation;rest-v1-integrations.tscarries the four dispatchers (dispatchMockIntegration/dispatchHttpProxyIntegration/dispatchHttpIntegration/dispatchAwsLambdaIntegration) plussubstituteUriPlaceholders+applyRequestParameters. The CLI commands live atsrc/cli/commands/local-invoke.ts(creates thecdkd localparent + registersinvoke,start-api,run-task, andstart-service),src/cli/commands/local-start-api.ts,src/cli/commands/local-run-task.ts, andsrc/cli/commands/local-start-service.ts.src/cli/commands/local-state-loader.tsis a shared helper (extracted fromlocal-invoke.tsin PR #267) that bothcdkd local invoke --from-stateandcdkd local run-task --from-stateroute through to load cdkd's S3 state for the target stack — single impl, parameterized log prefix. It also exportsloadBootstrapContainerRepo(issue #1025): a best-effort, never-failing read of the region's asset-storage bootstrap marker (cdkd-bootstrap/{region}.json) thatcdkd local run-task --from-stateuses to recognize images published to a custom-named cdkd container-asset repo (cdkd bootstrap --container-repo, issue #1011) as cdk-asset images, keeping the localcdk.out-build fast path. Issue #606 layers aLocalStateProviderinterface (src/local/local-state-provider.ts) on top, with two implementations:s3-local-state-provider.tswrapslocal-state-loader.tsverbatim (the--from-statepath) andcfn-local-state-provider.tsreads a deployed CloudFormation stack viacloudformation:DescribeStackResources/DescribeStacks --Outputs/ListExports(paginated, memoized per substitution pass) for the new--from-cfn-stack [<cfn-stack-name>]flag — lets users runcdkd local invoke / start-api / run-task / start-serviceagainst CDK apps deployed via the upstream CDK CLI (cdk deploy→ CloudFormation) without first migrating to cdkd. The dispatcher lives atsrc/cli/commands/local-state-source.ts(createLocalStateProvider(options, cdkdStackName, synthRegion)returns the right provider for the supplied flags, enforces mutual exclusion between--from-stateand--from-cfn-stack, and resolves the bare-form--from-cfn-stackto the cdkd stack name verbatim). The dispatcher is a thin shim around thecdk-localnpm package:cdk-localowns the--from-cfn-stackimplementation + the dispatch logic, and cdkd injects its S3-backed--from-statefactory viacdk-local'sextraStateProvidershook. cdkd's owncfn-local-state-provider.tsis now dead code (kept in tree as a CAT-A shim candidate for a follow-up Phase 3 batch). Wire-format mapping for the CFn provider:Ref→DescribeStackResourceslookup;Fn::ImportValue→ListExports;Fn::GetAttis warn-and-dropped in v1 for most sites (CFn does not return per-attribute values fromDescribeStackResources), but as ofcdk-local@0.10.0a consumer Lambda's OWN env-varFn::GetAttvalues are recovered at runtime from the deployed function's already-resolved config (lambda:GetFunctionConfiguration) — CFn resolved every intrinsic at deploy time, so the function'sEnvironment.Variablesalready carries the concrete value; cdkd inherits this through thelocal-state-sourceshim (cdk-local'sCfnLocalStateProviderdoes the recovery; the optionalresolveDeployedFunctionEnvprovider method is implemented only on the CFn provider, so cdkd's S3--from-stateprovider is unaffected). OtherFn::GetAttsites (e.g. ECS container env) still warn-and-drop.Fn::GetStackOutputis rejected with a clear pointer (cdkd-specific intrinsic, no CFn equivalent). Region handling reuses--stack-region— no separate--cfn-stack-regionflag. Phase 3 shim swap (Batch B): an expanding set ofsrc/local/**modules are now thin re-export shims (export { ... } from 'cdk-local') — the implementations described above are owned bycdk-local(which exposes the symbols from its package entry) and cdkd consumes them verbatim instead of carrying byte-identical copies; their unit tests moved to cdk-local alongside the implementations. Because cdkd keeps its OWNcdkd localcommand tree (it does NOT use cdk-local's command factories, which install the host embed-config themselves),createLocalCommand()(insrc/cli/commands/local-invoke.ts) callssetEmbedConfig(CDKD_EMBED_CONFIG)once at build time so every shim that reads cdk-local'sgetEmbedConfig()renders cdkd branding (cliName: 'cdkd local'/resourceNamePrefix: 'cdkd-local'/awsBindMountPath: '/cdkd-aws'/envPrefix: 'CDKD') instead of cdk-local'scdkldefaults — cdk-local0.20.0(cdk-local#85) exposessetEmbedConfigfrom its package entry for exactly this shim-host case. Slice 1 (cdk-local@0.8.0):intrinsic-utils.ts,intrinsic-lambda-arn.ts,parameter-mapping.ts,api-gateway-response.ts,docker-inspect.ts. Slice 2 / route cluster (cdk-local@0.11.0):route-discovery.ts,route-matcher.ts,api-gateway-event.ts,websocket-route-discovery.ts. Slice 3 / authorizer leaves (cdk-local@0.12.0):authorizer-cache.ts,cognito-jwt.ts. Slice 4 / leaf utilities (cdk-local@0.14.0):env-resolver.ts,stage-resolver.ts. Slice 5 /cloud-map-registry.ts(cdk-local@0.15.0): the slice-4 candidate that had to wait forcdk-local@0.15.0(cdk-local#79) to addtype RegistrationHandleto its package entry — the still-local siblingsrc/local/ecs-service-runner.tsimports that type via./cloud-map-registry.js, so a bare-shim could not typecheck against0.14.0(which exposed only theCloudMapRegistryclass).cdk-local@0.14.0(cdk-local#78) had already ported thecloud-map-registryunit test alongside the class export, so cdkd's shim PR deleted the now-duplicate cdkd test. Slice 6 / leaf utilities (cdk-local@0.17.0):runtime-image.ts(resolveRuntimeImage/resolveRuntimeFileExtension/resolveRuntimeCodeMountPath— LambdaRuntime→ ECR base-image / source-file extension / in-container code-mount path),websocket-event.ts(buildConnectEvent/buildDisconnectEvent/buildMessageEvent+WebSocketHandshakeSnapshot/WebSocketLambdaEvent—$connect/$disconnect/ message event-shape builders),websocket-mgmt-api.ts(ConnectionRegistry/handleConnectionsRequest/parseConnectionsPath/buildMgmtEndpointEnvUrl+ConnectionRegistryEntry—@connectionsmanagement API: in-process connection registry + local management-endpoint HTTP handler).cdk-local@0.17.0(cdk-local#81) exposes these from its package entry + carries the three ported unit tests. The shim re-exports only the src-consumed symbols (test-only symbols likeresolveRuntimeSpec/UnsupportedRuntimeError/readRequestBodystay reachable via cdk-local's source for the ported tests, not the package entry);runtime-image's only divergence from cdk-local was anembedConfig-branded unknown-runtime error string the test does not assert on, so it shimmed cleanly. Slice 7 / leaf utilities (cdk-local@0.21.0):docker-version.ts(HOST_GATEWAY_MIN_VERSION/probeHostGatewaySupport— Docker host-gateway version probe gating the--add-host=...:host-gatewaymapping WebSocket Lambda containers need on Linux native dockerd; cdk-local#483 / issue #784 extended the re-export withresolveHostGatewayExtraHosts/HOST_DOCKER_INTERNAL_GATEWAY— the memoized never-throwinghost.docker.internal:host-gatewayresolver cdkd'slocal invoke/run-taskadopt so a Lambda / ECS container can reach a host-loopback server (AWS_ENDPOINT_URL_*/ tunneled VPC); merged into the runner's--add-hostlist byecs-task-runner.ts'smergeHostGatewayAddHostFlags, whilestart-service/start-albinherit it from cdk-local's ECS service emulator engine),api-server-grouping.ts(availableApiIdentifiers/filterRoutesByApiIdentifier/groupRoutesByServer+ApiServerGroup— splits a flat discovered-route list into one local HTTP server per RestApi / HTTP API / Function URL), andlayer-arn-materializer.ts(materializeLayerFromArn— downloads + unzips a literal-ARN Lambda Layer to a host tmpdir for/optbind-mounting).cdk-local@0.21.0(cdk-local#91) exposes these from its package entry + carries the three ported unit tests. The shim re-exports only the src-consumed symbols (test-onlyparseDockerVersion/compareDockerVersions/routeMatchesIdentifier/LayerMaterializationErrorstay reachable via cdk-local's source for the ported tests, not the package entry);docker-version+api-server-groupingwere byte-identical, andlayer-arn-materializer's only divergence was theembedConfig-branded tmpdir prefix (getEmbedConfig().resourceNamePrefixrenders cdkd'scdkd-localvia the host'ssetEmbedConfig, so behavior is identical). cdkd's consumer testslocal-invoke-layers.test.ts/local-start-api-layers.test.tskeep theirvi.mock('layer-arn-materializer.js')— direct module-replacement, so it still intercepts post-shim. Slice 8 / divergent leaves (cdk-local@0.22.0):cors-handler.ts(buildCorsConfigByApiId/buildCorsConfigFromCloudFrontChain/applyCorsResponseHeaders/matchPreflight+CorsConfig— CFnCorsConfiguration/ CloudFront-chain parsing + HTTP API v2 OPTIONS preflight) andintrinsic-image.ts(derivePseudoParametersFromRegion/tryResolveImageFnJoin/substituteImagePlaceholders+ImageResolutionContext— canonical CDK 2.xFn::JoinECR image-URI resolver + same-stack ECRFn::GetAttsynthesis).cdk-local@0.22.0(cdk-local#92) exposes these + carries the ported tests (cors-handler's test was MERGED into cdk-local's pre-existingisFunctionUrlOacFrontedcoverage — disjoint helper names, no collision; intrinsic-image's test added under its own filename alongside cdk-local'sintrinsic-image-ecr-getatt.test.ts). Both are clean SUPERSET inheritances: cdk-local's cors-handler adds anisFunctionUrlOacFrontedexport cdkd does NOT consume (a dead export — zero behavior change, lands unwired until the #63--strict-sigv4work adopts it); cdk-local's intrinsic-image adds a same-stack-ECRFn::GetAttArn / RepositoryUri synthesis that fires only for a bare-Fn::GetAttECR image URI under--from-cfn-stackwhere the canonicalFn::Joinpath (unchanged, already resolved pre-shim) did not —docs/local-emulation.md'srun-task/start-service--from-cfn-stackwarn-drop rows were narrowed to note that exception. The shim re-exports only src-consumed symbols (isFunctionUrlOacFronted/PreflightResponse/FnJoinResolveOutcomestay off the package entry). No breakers. Slice 9 / state-resolver (cdk-local@0.24.0):state-resolver.ts(substituteAgainstState/substituteAgainstStateAsync/substituteEnvVarsFromState/substituteEnvVarsFromStateAsync+ theCrossStackResolver/SubstitutionContext/StateEnvSubstitutionAudit/PseudoParameterstypes — the--from-state/--from-cfn-stackpure-functional intrinsic substituter for env-var / image / role / volume values;Ref/Fn::GetAtt/Fn::Sub/Fn::Join/Fn::Select/Fn::Splitplus asyncFn::ImportValue/Fn::GetStackOutputvia a cross-stack resolver, with per-key unresolved reasons).cdk-local@0.24.0(cdk-local#97) exposes these from its package entry + carries the ported module-own unit test (cdkd drops its copy). The shim re-exports only the src-consumed symbols (StateSubstitutionResult+ cdk-local's addedapplyDeployedEnvFallbackstay off the package entry — no cdkd consumer). A clean SUPERSET inheritance: cdk-local genericized the per-key unresolved-reason wording (no record in cdkd state→no record in the state source,via cdkd deploy→and ensure the producer stack was deployed,cdkd-managed stack→deployed stack,need --from-state context→need an active state source, e.g. --from-cfn-stack), so those USER-VISIBLE reason messages change wording on inherit (more accurate — they now cover--from-cfn-stacktoo); cdkd's two consumer-test reason-string assertions (local-start-api-from-state.test.ts,ecs-task-resolver.test.ts) were flipped to the new wording. No breakers (pure-functional; no namespace-spy / consumervi.mockof state-resolver or its transitive deps). Slice 10 / websocket-body (cdk-local@0.29.0):websocket-body.ts(bufferToBody— converts a ws-emitted message buffer into the AWS-canonical{ body, isBase64Encoded }event shape; text frames pass through as UTF-8, binary frames are base64-encoded).cdk-local@0.29.0(cdk-local#106) exposesbufferToBodyfrom its package entry + carries the ported module-own unit test; cdkd drops itsbufferToBody (B3 regression guard)block fromwebsocket-server.test.ts. UNLIKE every prior slice this is NOT a bareexport { bufferToBody } from 'cdk-local're-export but a thin spy-friendly LOCAL wrapper (export function bufferToBody(...) { return bufferToBodyImpl(...); }over the cdk-local impl): the still-localwebsocket-server.tsimportsbufferToBodyas a namespace (import * as websocketBody) and the B4 regression test (Issue #537 item 6) installsvi.spyOn(websocketBody, 'bufferToBody')to assert the post-$connect-deny close-handshake window does nobufferToBodyallocation work — a bare re-export binding is a non-configurable gettervi.spyOncannot redefine, so the wrapper preserves the spy seam while cdk-local owns the actual codec. No other breakers (pure-functional codec). Verified end-to-end via thelocal-start-api-websocketDocker integ. Slice 11 / cluster #4 (cdk-local@0.30.0):cloud-map-resolver.ts(buildCloudMapIndex+CloudMapIndex—start-serviceCloud Map service-discovery index fromAWS::ServiceDiscovery::PrivateDnsNamespace/::Service) andintegration-response-selector.ts(selectIntegrationResponse/evaluateResponseParameters/pickResponseTemplate/tryParseStatus+IntegrationResponseEntry— REST v1IntegrationResponses[]selection bySelectionPatternregex /ResponseParametersheader literals /Acceptcontent negotiation).cdk-local@0.30.0(cdk-local#109) exposes these + carries the two ported module-own unit tests (cdkd drops its copies). Both resolvers were byte-identical. The breaker here was NOT a mock seam but class identity (the third breaker family):cloud-map-resolverthrowsEcsTaskResolutionError(owned by still-localecs-task-resolver.ts) andintegration-response-selectorthrowsVtlEvaluationError(owned by still-localvtl-engine.ts); once the resolvers re-export from cdk-local their throws use cdk-local's BUNDLED error classes, while still-local consumers + tests (ecs-service-resolver.ts/ecs-service-resolver.test.ts/ecs-task-resolver.test.tstoThrow(EcsTaskResolutionError);rest-v1-integrations.ts'sinstanceof VtlEvaluationErrorcatch +vtl-engine.test.ts/rest-v1-integrations-issue-507.test.tsassertions) reference cdkd's LOCAL class — two distinct class objects across the package boundary, soinstanceof/toThrowwould silently fail. Resolved by the class-identity reconciliation: cdk-local#109 ALSO exportsEcsTaskResolutionError+VtlEvaluationErrorfrom its package entry, and cdkd's still-localecs-task-resolver.ts/vtl-engine.tsnow DELETE their localclassdefinitions andimport { ... } from 'cdk-local'+ re-export — their IMPLEMENTATION stays local but the error CLASS is sourced from cdk-local, so every throw site (local or shimmed) and every host-side assertion share ONE identity. This is the first slice to partially-couple a stay-local module to cdk-local (just the error class, not the impl). The shims re-export only src-consumed symbols (ResolvedCloudMapNamespace/ResolvedCloudMapService/SelectedIntegrationResponsestay off the package entry;integration-response-selector's oldexport { VtlEvaluationError }re-export is dropped — no cdkd consumer imported it from there). No mock-seam breakers (neither resolver is namespace-spied, and the rest-v1 consumer testsvi.mockrie-client.js, a dep ofrest-v1-integrationsitself, not of the shimmed selector). Verified end-to-end via thelocal-start-service+local-start-api-rest-v1-non-proxyDocker integs. Slice 12 / authorizer + sigv4 cluster (cdk-local@0.32.0): the cluster slice 11 marked deferred.http-server.ts(startApiServer/readMtlsMaterialsFromDisk+ServerState/StartedApiServer/MtlsServerConfig),authorizer-resolver.ts(attachAuthorizers+AuthorizerInfo/RouteWithAuth), andsigv4-verify.ts(defaultCredentialsLoader+CredentialsLoader) become re-export shims;lambda-authorizer.ts+authorizer-context.tsare DELETED (not shimmed) — oncehttp-server(their only importer) became a shim and their module-own tests moved to cdk-local, they had ZERO remaining cdkd consumers, so a re-export shim would have been dead code.cdk-local@0.32.0(cdk-local#113) exposes the consumed symbols + ports the http-server / authorizer-context test suites. The breaker was the #63 SigV4 default DIVERGENCE — the FOURTH breaker family: a deliberate behavior difference the host has not adopted, NOT a mock seam (see memoryfeedback_shim_blocked_by_unadopted_semantic_divergence): cdkd ships fail-closed-by-default (deny unverifiable AWS_IAM SigV4, security review #484) with an opt-OUT--allow-unverified-sigv4flag; cdk-local ships warn-and-pass-by-default with an opt-IN--strict-sigv4flag. A naive shim would flip cdkd's secure default to fail-open. Resolved WITHOUT a security regression and WITHOUT cdkd adopting cdk-local's default: (1) cdkd's still-locallocal-start-api.tstranslates its flag to cdk-local's existingsigV4StrictstartApiServeroption (sigV4Strict: options.allowUnverifiedSigv4 !== true— strict unless the opt-out flag is passed), so the deny/pass DECISION stays cdkd's fail-closed; (2) the SigV4 warn MESSAGES (emitted by cdk-local's bundledsigv4-verify) are parameterized via two new embedConfig fields — cdkd'sCDKD_EMBED_CONFIGsetssigV4StrictByDefault: true+sigV4OptFlag: '--allow-unverified-sigv4'so the inherited messages reference cdkd's opt-out flag + advice instead of cdk-local's--strict-sigv4(cdk-local#113 made the 4 flag-referencing messages polarity-aware; under cdk-local's defaults they render byte-identically to before). cdkd also cleanly GAINS cdk-local'soacFrontedFunction-URL exception (CloudFront re-signs OAC-fronted origin requests, so the local server can't verify a client signature — warn-and-pass is correct there; a behavior improvement). The mock-seam breaker slice 11 flagged (http-server.test.tsmockingrie-client.js'sinvokeRie) is resolved by those test suites moving into cdk-local (where rie-client is in-bundle mockable). cdkd keepslocal-start-api.tslocal (its--allow-unverified-sigv4flag + the option translation + the cdkd-gluelocal-embed-config.test.ts), so the cdkd CLI surface is unchanged. Verified end-to-end via thelocal-start-apiDocker integ. UPDATE 2026-05-31 (case-A → case-B retrofit): the case-A divergence-preserving resolution was REVERSED with user sign-off — cdkd now follows cdk-local's warn-and-pass default.CDKD_EMBED_CONFIGflipped tosigV4StrictByDefault: false+sigV4OptFlag: '--strict-sigv4';LocalStartApiOptions.allowUnverifiedSigv4 → strictSigv4; the twolocal-start-api.tstranslation sites flipped tosigV4Strict: options.strictSigv4 === true; the CLI option renamed--allow-unverified-sigv4 → --strict-sigv4with the inverted help text + default; shim header comments inhttp-server.ts/sigv4-verify.tsupdated. BREAKING CHANGE for users who relied on cdkd's prior fail-closed default — they must now pass--strict-sigv4to opt in. Slice 13 / docker-image-builder (cdk-local@0.33.0):docker-image-builder.ts(buildContainerImage+architectureToPlatform+BuildContainerImageOptions—invokelocal container-Lambda build).cdk-local@0.33.0(cdk-local#114) exposes these +LocalInvokeBuildError; cdk-local#115 ports the executable-source re-tag test cases. UNLIKE the bare re-exports, this is a BOUNDARY-WRAPPER shim (like slice 10's spy wrapper): the slice-12 note flagged docker-image-builder BLOCKED because itsLocalInvokeBuildError extends CdkdError(cdkd's base) while cdk-local's isCdkLocalError-based, so the slice-11 same-base class-identity reconciliation cannot apply. The fix is a thin wrapper —architectureToPlatform+ theBuildContainerImageOptionstype re-export directly, butbuildContainerImageis wrapped to catch cdk-local's thrownLocalInvokeBuildErrorand re-throw cdkd'sCdkdError-based one at the boundary, so a local-invoke build failure still surfaces with cdkd's exit code / formatting.ecr-puller+ecs-task-runnerthrow / catch their OWNLocalInvokeBuildError(self-contained — they do NOT call docker-image-builder), so they are unaffected. The cdkddocker-build-executable-retag.test.ts'sdocker-image-builderre-tag block moved to cdk-local (cdk-local#115); the file'sdocker-asset-publisherblock (cdkd ECR publish path, stay-local) stays. Verified end-to-end via thelocal-invoke-containerDocker integ. Slice 14 / file-watcher (cdk-local@0.34.0):file-watcher.tsbecomes a bare re-export shim (createFileWatcher+FileWatcher/FileWatcherOptionstypes). UNLIKE every prior shim this was a user-approved BEHAVIOR-CHANGING feature reconciliation, not a mechanical re-export:cdkd local start-api --watchflips from cdkd's watch-OUTPUT model (watchcdk.out/+ asset dirs; reload only when something else re-synths) to cdk-local's watch-SOURCE model (watch the CDK app source tree atprocess.cwd(), excludecdk.out/node_modules/.git, honorcdk.jsonwatch.include/watch.exclude, and RE-SYNTH on a source edit — thecdk watch-like UX). The change was small because cdkd'sreloadAllServersALREADY re-synths (synthesizeAndBuild), so it was a watch-TARGET swap, not a re-synth retrofit.cdk-local@0.34.0(cdk-local#116) exposescreateFileWatcher/FileWatcher/FileWatcherOptions+createWatchPredicates/WatchPredicates+resolveWatchConfig/CdkWatchConfig. cdkd's still-locallocal-start-api.tsimportscreateWatchPredicates+resolveWatchConfigfromcdk-local, watches[process.cwd()]with cdk-local'signored/shouldTriggerpredicates, and DELETES the watch-output plumbing (computeAssetPaths,lastAssetPaths, theFileWatcher.update()dynamic-path calls, and the correspondingreloadAllServersargs). cdkd'sfile-watcher.test.tsdrops (cdk-local owns it). No self-fire loop: cdkd's synth writes only tocdk.out, whichcreateWatchPredicatesexcludes. Verified end-to-end via thelocal-start-apiDocker integ. The Phase 3 shim swap is COMPLETE — every shimmablesrc/local/**module is now a re-export shim (or a boundary / spy wrapper); the only modules that remain cdkd-local are the stay-local-FOREVER set (theecs-*engine,rie-client,container-pool,lambda-resolver,ecr-puller,docker-runner,reload-orchestrator,httpv2-service-integration,websocket-server,rest-v1-integrations,vtl-engine+ecs-task-resolver[impl local; their error class is sourced from cdk-local per slice 11], and the*-local-state-providerplumbing) plus the CLI command files that keep cdkd's own command tree. NOTEroute-discovery.ts's error strings still emit ago-to-k/cdkddocs URL via cdk-local until the cdk-local self-containment cleanup parameterizes it viaembedConfig; until then cdkd's shim keeps emitting the cdkd URL (correct for cdkd).cdkd local start-alb(#86) ships as a thin shim consumer of the shared ECS service emulator engine —src/local/elb-front-door-resolver.tsre-exportsresolveAlbFrontDoor/isApplicationLoadBalancer+ the front-door type set fromcdk-local,src/cli/commands/ecs-service-emulator.tsre-exportsrunEcsServiceEmulator/addCommonEcsServiceOptions+ the engine'sEcsServiceEmulatorOptions/EmulatorStrategy/Planned*types fromcdk-local/internal, and the command filesrc/cli/commands/local-start-alb.ts(createLocalStartAlbCommand) wires itsLocalStartAlbOptions(cdkd-specific--from-state/--state-bucket/--state-prefix+tls?: booleanextending the engine'sEcsServiceEmulatorOptions) intorunEcsServiceEmulator(targets, options, albStrategy(options), cdkdExtraStateProviders). The ALB-specific flags (--lb-port/--tls/--tls-cert/--tls-key/--no-verify-auth/--bearer-token) are registered via cdk-local'saddAlbSpecificOptions(cmd)(added in cdk-local 0.64.0 / cdk-local#203) so cdkd auto-inherits any new ALB-only flag the upstreamcdkl start-albadds without manual.addOption(...)duplication;parseLbPortOverrides/resolveAlbTarget/albStrategylive in cdk-local and are re-exported by cdkd'secs-service-emulator.tsshim. BREAKING 2026-05-31: cdk-local 0.64.0 flips the default HTTPS-listener local behavior from auto-TLS-terminate (with self-signed cert) to plain HTTP (withX-Forwarded-Proto: httpspreserved); cdkd inherits the new default. Users who want the prior behavior must pass--tls(auto-generates self-signed cert) or--tls-cert/--tls-key(user-supplied cert). The 4th-argextraStateProvidersis sourced from the new exportcdkdExtraStateProvidersinsrc/cli/commands/local-state-source.ts({ fromState: fromStateFactory }) — the same factorycreateLocalStateProviderregisters for the rest of thecdkd local *family — so cdk-local's engine picks cdkd's S3-backed--from-statefactory transparently when it callscreateLocalStateProviderinternally per backing-service boot.cdkd local start-service(Part B follow-up to PR #725, 2026-05-30) ships as the second consumer of the same shared engine —src/cli/commands/local-start-service.tscollapses from a 944-line per-replica orchestrator to a ~120-line shim mirroringlocal-start-alb.ts's shape (LocalStartServiceOptionsextendsEcsServiceEmulatorOptionswith cdkd's--from-state/--state-bucket/--state-prefix, aserviceStrategy(options): EmulatorStrategyreturnsbootsonly with emptylbPortOverridesand nofrontDoor, andcreateLocalStartServiceCommandwiresrunEcsServiceEmulator(targets, options, serviceStrategy(options), cdkdExtraStateProviders)). The start-service-specific flags (--host-portsince cdk-local 0.62.0;--watchsince cdk-local 0.69.0 / cdk-local#214 Phase 4) are registered via cdk-local'saddStartServiceSpecificOptions(cmd)so cdkd auto-inherits any new start-service-only flag the upstreamcdkl start-serviceadds without manual.addOption(...)duplication.--watchon eitherstart-serviceorstart-albruns the cdk-local engine's Phase 4 classifier per reload: source-only edits on interpreted-language handlers (Node / Python / Ruby / shell) inside a CDK image asset take a bind-mount FAST PATH (docker cp+docker restart, nodocker build, typical end-to-end latency well under a second), while Dockerfile / dependency manifest / compiled-language source / asset-hash-unchanged / ambiguous edits fall through to the Phase 1-3 rebuild rolling primitive (shadow boot + atomic Service Connect / Cloud Map / front-door pool swap); the classifier verdict + per-replica completion lines (verdict=soft-reload/Soft-reloaded replica ... restart + TCP-ready probe completevsverdict=rebuild (...)/Rolling replica ... swap complete) are emitted by the engine directly and pass through cdkd's output unchanged. The fixture exercising both paths against real Docker istests/integration/local-start-service-watch-fast/. The retainedsrc/local/ecs-network.tsexports —createTaskNetwork/destroyTaskNetwork/buildMetadataEnv/buildEndpointSubnet/METADATA_ENDPOINT_IMAGE/METADATA_ENDPOINT_IP— are kept ONLY becauseecs-task-runner.ts(the still-localcdkd local run-taskorchestrator) consumes them; oncerun-taskmigrates to a cdk-local engine of its own those exports become deletable too.
- src/cli/config-loader.ts - Config resolution (cdk.json, env vars for
--appand--state-bucket) - src/cli/stack-matcher.ts - Shared stack-name matcher used by deploy/diff/destroy/list. Routes patterns by whether they contain
/(display-path) or not (physical name) and returns a deduplicated union. - src/cli/program.ts -
buildProgram()— builds the fullcdkdCommander tree (everycreate*Command()factory,.name/.description/.version). Split out ofindex.tsfor the same reasonpipe-close-handler.tswas: importingindex.tsrunsmain()as a side effect, so tooling could not read the command tree without executing the CLI.index.ts'smain()now calls it. The consumer that motivated the split isscripts/check-integ-cli-flags.ts(viatests/unit/scripts/integ-cli-flags.test.ts), which validates every integ-fixture CLI invocation against the option set of the subcommand that actually declares the flag — a check that needs the REAL tree, because--helpomits hidden options andsrc/cli/options.tsis a flat global list carrying no command attachment (thecdkd import --regionbug, issue #1097). - src/cli/pipe-close-handler.ts -
installPipeCloseHandler()— attaches an'error'listener toprocess.stdout/process.stderrso a downstream consumer closing the pipe early (cdkd state list | grep -q,... | head) exits the CLI cleanly (process.exit(0)on EPIPE) instead of crashing with an unhandled-'error'stack trace; non-EPIPE stream errors re-throw unchanged. Called once at the top ofmain()insrc/cli/index.ts. Kept in its own module (not inline inindex.ts) so it stays unit-testable — importingindex.tsrunsmain()as a side effect. - src/cli/commands/diff-recursive.ts - Recursive nested-stack diff helpers backing
cdkd diff --recursive(issue #555 A5). OwnsbuildDiffTree(walks eachAWS::CloudFormation::Stackrow → child synth template + child state atcdkd/<parent>~<childLogicalId>/<region>/state.json, recursing into grandchildren; children are the union of template nested rows (CREATE/UPDATE, descend via template) and state-only nested rows (DELETE, descend via state vs empty template) so the tree previews the full next deploy),computeStackDiff(the per-stack state-vs-template diff extracted so the top-level loop and the walker share one impl; mirrors the deploy engine's parameter/condition preprocessing best-effort — binds templateParametersdefaults viaresolveParameterswith the nested-stack input parameters as user values, evaluatesConditions, prunes condition-false resources viafilterResourcesByCondition, and threadsparameters+conditionsinto the resolver context soRef/Fn::Sub/Fn::FindInMap/Fn::Ifresolve like they do on deploy — issue #1027; binding failures fall back to the raw-template diff),readNestedTemplate/indexNestedChildTemplates(template-file loaders mirroringNestedStackProvider's private copies — duplicated to keep the CLI layer off the provisioning layer),nodeHasChanges/treeHasChanges(real-change detectors powering--fail),diffTreeToJson(nested--jsonshape,NO_CHANGEdropped,childrenalways present), andrenderChangeLines/renderDiffTree(the human text renderer, moved out ofdiff.tsso it is unit-testable without the synth/AWS-client pipeline; a property side whose WHOLE value is an unresolved intrinsic — aRef/Fn::GetAttto a resource the same deploy will CREATE — renders as the compact raw intrinsic annotated(known after deploy)instead of collapsing toundefined, and the diff's best-effort resolver contexts setResolverContext.bestEffortso the resolver's Ref-not-found log is debug there, warn on deploy-time resolution — issue #1017).diff.tsis now thin glue: synth →buildDiffTreeper target stack → render / JSON /--fail. NOT in theinteg-destroymarkgate scope (diff never destroys).cdkd diff --failexits 1 on any change (CDK parity withcdk diff --fail); plaincdkd diffalways exits 0. - src/cli/commands/events.ts + src/state/deployment-events-store.ts + src/types/deployment-events.ts - Structured deployment events (issue #808) — cdkd's
DescribeStackEventsequivalent.deployment-events.tsdefines theDeploymentEvent/DeploymentEventRecordertypes +extractDeploymentEventError(walks the thrown error's.causechain for AWS error code / request id).deployment-events-store.tsownsDeploymentEventsStore(the buffering JSONL recorder injected intoDeployEngineOptions.eventRecorder/DestroyRunnerContext.eventRecorder— best-effort async flush, never blocks the run, warns once on S3 failure) andDeploymentEventsReader(the read side: region discovery via raw key listing so it survives destroy, run listing, single-run JSONL parse).events.tsis the state-driven (no synth, no lock)cdkd events <stack> [--run <id>] [--format json] [--stack-region <r>]command. S3 layout:cdkd/{stackName}/{region}/deployments/{runId}.jsonl+deployments/index.json(last N runs, last-writer-wins; SEPARATE key family fromstate.json— no state schema bump). Events carry error + metadata only, never resource properties. The per-resource + rollback events are emitted byDeployEngine(provisionResource/ the sharedsrc/deployment/rollback-executor.ts) +destroy-runner.ts's delete loop; the run-level RUN_STARTED / RUN_FINISHED +finalize()are owned bydeploy.ts/destroy.tsvia the sharedsrc/cli/commands/deployment-events-run.tsbracket helpers (startRunRecorder— returnsundefinedunder--dry-runso no recorder / events;recordRunSucceeded/recordRunFailed). Since issue #1183 the standalonecdkd rollbackcommand ALSO opens a recorder (command: 'rollback', an additiveDeploymentRunCommandliteral) and emitsROLLBACK_*events under its own runId. The reader's index-fallback (whenindex.jsonis missing / corrupt) derives each run's result from its own JSONL's lastRUN_FINISHEDevent and reportsUNKNOWN(aDeploymentRunSummaryResultvalue) for a stream with none — never fabricatingFAILED. Retention / purge (issue #885): thedeployments/prefix is kept bounded two ways — (1) the writer self-bounds atfinalize()viapruneSupersededRunFiles, deleting{runId}.jsonlstreams that fell out of the 20-run index window (best-effort inside the same write-chain link, never blocks the run; concurrency-safe because it only deletes ids strictly older than the oldest retained, time-sortable id); (2)cdkd events prune <stack>(createEventsPruneCommand/eventsPruneCommand) is the explicit user-initiated purge (--all/--keep <N>/--older-than <dur>/ default keep-20,-yto skip the confirm), routed throughDeploymentEventsReader.pruneRunswhich deletes the matching streams + rewrites (or removes, when empty)index.json. Both batch-delete via the newS3StateBackend.deleteRawObjects(keys)(chunked to the 1,000-keyDeleteObjectsceiling, idempotent).runIdTimestampMsparses a run id's compact-ISO prefix back to epoch ms for the--older-thancutoff. (3)cdkd destroy --purge-events(destroy-only flag) deletes a stack's event history right after a CLEAN, non-interrupted destroy via the exportedpurgeEventsAfterDestroy(reader, stack, region, {purgeEvents, runResult, interrupted}, logger)gating helper indestroy.ts— best-effort warn-on-failure; skipped on a failed/interrupted destroy so those events stay as post-mortem; called AFTER the run'seventRecorder.finalize()so this run's own events are included in the purge.state destroydoes not take the flag (cdkd events prune <stack> --allis the equivalent). Full guide in docs/deployment-events.md. - src/cli/commands/rollback.ts -
cdkd rollback [STACK](issue #1183): the state-driven, synth-free command that reverts a stack to its pre-deploy state after a failed--no-rollback/ interrupted deploy (the cdkd equivalent ofcdk rollback/ CFnRollbackStack). Loads therollback-journal.json(written by the deploy engine at failure time), prints a per-segment plan, and replays it newest-first viasrc/deployment/rollback-executor.ts, saving state after each op and popping each cleanly-replayed segment; when the oldest segment was the first-ever deploy and state ends empty,state.jsonis deleted too. ReusessetupStateBackend/resolveSingleRegion(exported fromstate.ts) +startRunRecorder(command: 'rollback'). Flags:--force,--orphan <logicalId>(repeatable),--revert-failed(issue #1198 — opt-in replay of the segment's journaledfailedOperationsBEFORE its completed ops: failed UPDATE force-reverted topreviousStatewith the ATTEMPTED properties as the diff's previous side, failed CREATE deleted only when a state record matches — and then under itsDeletionPolicy(issue #1362:Retainorphans,Snapshotsnapshots-then-deletes with--skip-final-snapshotas the opt-out), failed DELETE a no-op; off by default because the failed resource's remote state is unknown; usable in the DEFAULT deploy flow since issue #1208 — a CLEAN automatic rollback settles the journal to a failed-only segment (operations: []+failedOperations,reason: auto-rollback-clean) instead of deleting it, and the next deploy's journal note points at--revert-failedfor that shape),--skip-final-snapshot(issue #1358 — data-loss opt-out for a rolled-back CREATE underDeletionPolicy: Snapshot, which otherwise snapshots then deletes; the command also builds stack-region-pinnedAwsClientsfor the pre-delete snapshot calls when the target stack's region differs from the CLI's),--stack-region,--role-arn,--state-bucket. Exit codes: 0 clean / 2 partial (journal kept, idempotent re-run) / 1 hard error. No-arg picks the single journaled stack (else lists candidates via alistRawKeysscan forrollback-journal.json). - src/cli/commands/gc.ts -
cdkd gc(issue #1012): garbage-collects unreferenced objects/images from ONE region's cdkd-owned asset storage (names from the bootstrap marker, never the naming convention; CDK bootstrap storage untouched). Scans EVERY state file in the whole state bucket for{S3Bucket,S3Key}pairs /s3://URIs / https URLs / ECR tag+digest URIs; guards: lock.json abort, malformed-state abort,--older-thanage guard (default 30d),ExpectedBucketOwneron every S3 call;--dry-runplan, y/N confirm, chunkedDeleteObjects(1,000) /BatchDeleteImage(100). Sharessrc/cli/commands/state-file-keys.ts(whole-bucket state/lock key listing +stack (region)descriptor, extracted frombootstrap-destroy.ts) so the two commands' state discovery cannot drift. - src/cli/commands/state-list-tree.ts - Pure-functional helpers backing
cdkd state list --tree(issue #555 A3). OwnsbuildStackTree(flat(stackName, region, parentStack, parentRegion)list → parent → child tree, orphan-child promotion + self-link defense),renderStackTreeAscii(tree(1)-style box-drawing├──/└──/│prefixes), andstackTreeToJson(nested shape for--tree --jsonwith explicitnullfor absent parent fields). Kept separate fromstate.tsso the tree-construction logic stays unit-testable without mockingS3StateBackend. The S3 read fan-out (onegetStateper ref) happens instate.ts'srenderTreeModewrapper — the helper itself is sync. - src/cli/yaml-cfn.ts - CFn-aware YAML codec used by
cdkd exportandcdkd import --migrate-from-cloudformation. Parses + serializes CloudFormation templates while preserving every CFn shorthand intrinsic tag (!Ref/!GetAtt/!Sub/!Join/!Select/!Split/!If/!Equals/!And/!Or/!Not/!FindInMap/!Base64/!Cidr/!GetAZs/!ImportValue/!Transform/!Condition). Built on theyamlpackage's custom-tag schema; each tag parses to its long-form{Fn::Foo: <args>}object (or{Ref: <name>}for!Ref) so every downstream consumer reads one canonical representation, and re-emits back to the same shorthand tag on YAML stringify. Format auto-detection sniffs the first non-whitespace byte ({/[→ JSON; anything else → YAML). - src/synthesis/app-executor.ts - Executes CDK app as subprocess with proper env vars (CDK_OUTDIR, CDK_CONTEXT_JSON, CDK_DEFAULT_REGION, etc.)
- src/synthesis/assembly-reader.ts - Reads and parses Cloud Assembly manifest.json directly
- src/synthesis/synthesizer.ts - Orchestrates synthesis with context provider loop. After the loop settles, routes any template that {@link containsMacro} flags through
src/synthesis/macro-expander.tsBEFORE returning to the analyzer / provisioner pipeline (Issue #463). Since issue #1150 the pass is selection-aware:SynthesisOptions.deferMacroExpansionskips it insidesynthesize(), and the now-publicexpandMacrosForStacks(stacks, options)is invoked bydeploy/diffAFTER stack selection with only the stacks they will consume (a macro-carrying sibling outside the selection never triggers a CFn round-trip);listanddestroydefer and never expand (names come from the manifest, destroy works off cdkd state). Macro region resolution falls back to the AWS SDK default chain (resolveSdkDefaultRegion- shared config profile region etc.) before hard-erroring (issue #1149), and the STS hop for the default state bucket only runs when a selected stack actually carries a macro. - src/synthesis/macro-detector.ts - Pure-functional
containsMacro(template)/enumerateMacros(template)helpers (Issue #463). Detect top-levelTransform: [...]AND nestedFn::Transform: {...}blocks anywhere underResources/Outputs/Mappings/Conditions/Rules. SkipMetadatakeys at any depth (CFn does not expand transforms inside metadata). Tolerate malformed inputs without throwing so the rest of the synthesis pipeline surfaces the malformed-template error. - src/synthesis/macro-expander.ts - CloudFormation macro round-trip helper (Issue #463 Phase 2; design at docs/design/463-cfn-macros.md). Issues a transient
CreateChangeSet --change-set-type CREATE(which auto-creates the stack inREVIEW_IN_PROGRESS, no priorcdkd-macro-expand-*stack needed — Q1 empirically verified 2026-05-23), waits forChangeSetStatus: CREATE_COMPLETE, fetchesGetTemplate --template-stage Processed(returns the post-expansion template; the SDK types the field asstring | undefinedbut the wire shape may be a parsed object — the helper handles both), and cleans up viaDeleteChangeSet+DeleteStackin afinallyblock (both NotFound-tolerant). For templates that declareParameterswithoutDefault, passes synthetic placeholder values (CFn rejectsCreateChangeSetotherwise; the values do NOT leak into the Processed-stage template —Ref: <param>survives intact for cdkd's own resolver). InlineTemplateBodyfor templates <= 51,200 bytes; uploads to the cdkd state bucket and submitsTemplateURLfor (51,200, 1 MB]; refuses outright above 1 MB. Multi-stage macros (an expanded template that still contains a macro) hard-error with a clear pointer at the design's "out of scope for v1" note. ThrowsMacroExpansionError(exit code 2) on every failure mode. IntermittentAWS::EarlyValidation::*hook rejections of the transient changeset (issue #1151) are retried up to 3 attempts with a fresh transient stack name and 2s/4s backoff (retryDelays.sleepis the test seam) before the error surfaces. - src/synthesis/stack-messages.ts - CDK annotation-message handling (issues #1228 / #1230).
collectStackMessages(assemblyDir, artifact)gathersaws:cdk:error/aws:cdk:warning/aws:cdk:infoentries from the artifact's inlinemetadataAND itsadditionalMetadataFileside file (<artifactId>.metadata.json— the layout current aws-cdk-lib uses instead of inlining; unreadable or wrong-shape referenced side file throws, fail-closed) intoStackInfo.messages.processStackMessages(stacks, logger, options?)is the CDK-CLI-parity gate: logs every message at its level ([Error|Warning|Info at /path] …), throwsSynthesisError('Found errors')when any given stack carries an error annotation;StackMessageOptions.strict(the--strictflag) additionally throwsSynthesisError('Found warnings (--strict mode)')on warnings,ignoreErrors(--ignore-errors) displays-but-never-throws, strict wins over ignoreErrors (CDK CLI failAt precedence). Wired intosynth(all stacks) anddeploy(final selection, before macro expansion / AWS mutations) via the sharedannotationMessageOptionsinsrc/cli/options.ts; other synth-driven commands intentionally unaffected. - src/synthesis/context-providers/ - Context providers (see
src/synthesis/context-providers/for full list) for missing context resolution - src/cli/commands/drift.ts -
cdkd drift [<stack>...]implementation. State-driven (no synth). Reads cdkd state from S3, asks each provider's optionalreadCurrentStatefor the AWS-current snapshot, and pipes the result throughsrc/analyzer/drift-calculator.ts. Auto-selects the single stack in state when no positional arg /--allis given (mirrorscdkd deploy/cdkd destroy); errors with a listing when state has more than one stack. Exits 0 on no drift, 1 on drift detected, 2 on error.--accept/--revertare deferred to a follow-up PR. - src/analyzer/drift-calculator.ts - State-vs-AWS property comparator used by
cdkd drift. Only descends into keys present in cdkd state, so AWS-managed fields cdkd never set (timestamps, generated identifiers, account-wide defaults) cannot surface as false-positive drift. Accepts an optionalignorePathslist (sourced from each provider'sgetDriftUnknownPaths) to skip state property paths the provider deliberately cannot read back from AWS — e.g. LambdaCode: { S3Bucket, S3Key }, whichGetFunctiononly returns as a pre-signed URL — so a clean run reports no drift on those keys instead of the guaranteed false positive that would otherwise fire on every invocation. Before any comparison it canonicalizes both the baseline and AWS-current sides throughsrc/analyzer/drift-normalize.tsso that AWS returning a tag list, a resource-id/ARN array, or a provider-declared unordered plain-string set (options.unorderedPaths, sourced fromgetDriftUnorderedPaths) in a different order than the deploy-time snapshot does not surface as phantom drift (thedeepEqualwalk compares arrays positionally). - src/analyzer/drift-normalize.ts - Order-normalization helpers for
drift-calculator.ts.canonicalizeTagListsDeepsorts any{Key,...}[]tag list byKey;canonicalizeIdArraysDeepsorts any array whose every element is an AWS resource id (subnet-…,sg-…) or ARN. Both recurse and are applied to BOTH comparison sides — AWS does not guarantee element ordering across reads, and these two kinds are semantically unordered sets, so without this every reorder would be a false positive. Surfaced by dogfooding the siblingcdk-real-drifttool. Plain-string arrays are NOT canonicalized heuristically (a scalar list can be order-significant); insteadcanonicalizeUnorderedArraysAtPaths(value, paths)sorts a plain-string array ONLY at an explicit per-provider opt-in path list, sourced from the new optionalResourceProvider.getDriftUnorderedPaths(resourceType)and threaded throughcalculateResourceDrift'soptions.unorderedPathsbydrift.ts(issue #1096 item 1). Both provider-declared path lists share ONE matcher — the exportedmatchesPathPrefix(path, entries)(exact match, or entry followed by.), whichdrift-calculator.ts'sisIgnoredPathis now a thin alias over — so the two cannot silently drift apart. Every entry is a SUBTREE declaration; there is no leaf-only form. One required divergence:isIgnoredPathnever sees a path crossing an array (the comparator compares arrays wholesale viadeepEqual), whereas the unordered walk descends into array elements and gives them the parent's path, so'Items.Aliases'is meaningful forgetDriftUnorderedPathsbut inert as an ignore-path — strictly more permissive. Nested arrays are not descended into, so an array-of-arrays at a declared path never has its inner lists sorted.FSxFileSystemProviderdeclares onlyWindowsConfiguration.Aliases;SelfManagedActiveDirectoryConfiguration.DnsIpsis deliberately NOT declared (AWS documents no set semantics for it and DNS resolver lists are conventionally preference-ordered — declaring an order-significant list would silently HIDE real drift, which is worse than the visible false positive of leaving it undeclared; same reasoning excludes ElastiCachePreferredAvailabilityZones). Declaring the path here rather than sorting inside the provider'sreadCurrentStatereverse-mapper is load-bearing: the normalizer runs on BOTH sides, so it stays correct for theproperties-fallback baseline (a resource deployed before observed-capture, whose baseline is the user's template order) — sorting only the read side would manufacture drift there. - src/deployment/dag-executor.ts - Generic event-driven DAG dispatcher (used inside a stack to schedule resource provisioning as soon as each resource's deps complete; no level barriers)
- src/deployment/rollback-executor.ts - Reusable rollback engine (issue #1183), extracted from
DeployEngineso BOTH the in-process automatic rollback AND the standalonecdkd rollbackcommand drive identical semantics. Owns theCompletedOperation/FailedOperationtypes (the former moved here fromdeploy-engine.ts),replayRollback(reverts a list of ops: UPDATE/DELETE reverse-completion-order, then CREATE deletions dependency-sorted; best-effort per-op),classifyRollbackOp/planRollback(pure classification used by the command's plan preview; each plan item also carrieseffectiveProvisionedBy— the record-first route resolution — so the preview can consult the SAMEfinalSnapshotMechanismmatrix the replay runs and label a Snapshot delete that will be REFUSED instead of promising a snapshot, issue #1366),classifyFailedOp/planFailedOps/replayFailedOperations(issue #1198 — the--revert-failedopt-in path for the op that FAILED mid-deploy; its delete of a provisioned-but-failed CREATE honors the CURRENT state record'sDeletionPolicythrough the SAME matrix as the completed-CREATE path —orphan-failed-create-retainforRetain,delete-failed-create-with-final-snapshotforSnapshot, plaindelete-failed-createotherwise, issue #1362), andsortRollbackCreates. A replacement op (previousState.physicalId !== op.physicalId) is reverted by REVERSING the replacement (issue #1199): re-create the old resource frompreviousStatevia its recordedprovisionedByroute then delete the new one (create-first; name collision falls back to delete-new-first + bounded name-release retry), or — underUpdateReplacePolicy: Retain, where the old resource was orphaned not destroyed — delete the new one and re-adopt the old (reverse-replacement-readopt); stateful types warn that the old data is unrecoverable. Two deliberate behavior fixes shared by both callers: the rolled-back CREATE's CURRENT state recordDeletionPolicygoverns its delete (CFn semantics) —RetainORPHANS (dropped from state, left in AWS),Snapshotroutes to thedelete-with-final-snapshotaction which snapshots THEN deletes through the same mechanism matrix as the deploy engine'sprepareFinalSnapshotForDelete(atomic delete parameter on the SDK route,createPreDeleteFinalSnapshotforPRE_DELETE_SNAPSHOT_TYPES, refusal-as-per-op-failure for a cc-api-routed atomic type or any other Snapshot-tagged shape) unlessRollbackExecutorContext.skipFinalSnapshot(cdkd rollback --skip-final-snapshot) opts into the data loss — issue #1358, which fixed the pre-existing leak whereSnapshotorphaned alongsideRetainand handed the user an untracked, billing resource;RetainExceptOnCreateand absent /Deletedelete plainly — and replay is idempotent (skips already-reverted / physical-id-mismatched / absent resources). Depends only onProviderRegistry+ region + logger + an optional event recorder / per-op state-save hook /finalSnapshotClients(region-pinnedPreDeleteSnapshotClients, falling back togetAwsClients()) /skipFinalSnapshot— NOT onDagBuilder/DiffCalculator/ the synthesizer /ExportIndexStore. - src/deployment/work-graph.ts - WorkGraph DAG orchestrator for asset publishing and stack deployment
- src/deployment/retryable-errors.ts - Shared transient-error classifier (HTTP 429/503 + message-pattern table covering IAM/CW Logs/SQS/KMS/etc. propagation delays). Consumed by
withRetryinsrc/deployment/retry.tsto decide whether to back off and retry vs. fail fast. The message table is stored as two composed halves —IAM_PROPAGATION_ERROR_MESSAGE_PATTERNS(just-created IAM entity not yet visible to a service's authorization layer) plus a private non-propagation half — spread into the single exportedRETRYABLE_ERROR_MESSAGE_PATTERNS, so retryability has ONE source of truth whileisIamPropagationError(message)can select the DENSE retry cadence for the propagation class (seeretry.ts). A misfiled pattern only changes the cadence, never whether the error is retryable. ExportsisThrottlingError(error)— the single bounded error +.causewalk for rate-limit signals (throttling error NAMES and retryable HTTP statuses, both checked at every depth up to 5).isRetryableTransientErrorlayers the message-pattern table on top of it. Also exportsisNameCollisionError(message)(issue #1207) — the "already exists" name-collision matcher shared by the deploy engine's replacement create-first collision detection, its--replacedelete-first re-create retry, and the rollback executor's reverse-replacement collision detection + retry; deliberately NOT part of the transient pattern table (a collision is only retryable at the sites that just deleted the old name holder). Issue #1206 addsisNameCooldownError(message)(the SQSQueueDeletedRecently/ "wait 60 seconds" same-name re-creation cooldown — kept separate from the collision matcher because a cooldown at a create-first site must not trigger delete-new-first) andisRecreateRetryableError(message)(collision OR cooldown — the retry filter for the delete-then-re-create sites — the--replacedelete-first fallback, the--recreate-via-cc-api/--recreate-via-sdk-providerdestroy-then-create path (issue #1214; the inner generic retry's ~47s budget ends inside the 60s window), and the rollback executor's delete-new-first — paired with a maxRetries-8 / 10s-cap schedule ≈ 64s total sleep so the full 60s cooldown window is covered; the rollback executor's initial create-first attempt additionally retries the cooldown alone). - src/deployment/retry.ts - Retry helper used by DeployEngine. TWO schedules, picked per attempt from the error class: the generic 1s -> 2s -> 4s -> 8s capped at 8s over 8 retries (47s total sleep) for throttling and long resource-state transitions, and a DENSE 0.25s -> 0.5s -> 1s -> 2s -> 2s ... over 26 retries (47.75s total sleep,
IAM_PROPAGATION_{INITIAL_DELAY_MS,MAX_DELAY_MS,MAX_RETRIES}) for the IAM-propagation class (isIamPropagationError). Rationale: cdkd creates an IAM entity and consumes it ~1-3s later, so propagation failures resolve in single-digit seconds — the generic schedule's 4s/8s steps overshoot (a measured 3-instance EC2 stack burned ~10.2s of a 25.9s deploy in backoff afterInvalid IAM Instance Profile name), while throttling genuinely wants exponential backoff. The dense budget is deliberately >= the generic one so a denser probe grid never shrinks the window in which propagation can still be caught. The class is re-evaluated per attempt, so a throttle hit mid-propagation backs off exponentially. The dense schedule applies ONLY when the caller left the schedule at its defaults (the deploy engine's create/update path, drift revert, the ELBv2 / ServiceDiscovery attribute calls); any explicitmaxRetries/initialDelayMs/maxDelayMs/isRetryablemeans the caller owns the cadence and gets it verbatim (the DELETE path's 3 x 5s, the delete-then-re-create sites' ~64s SQS-cooldown budget,describe-type.ts's throttle-only retry). Delegates retryable-error classification toretryable-errors.ts. - src/provisioning/import-helpers.ts - Shared helpers for
ResourceProvider.import:resolveExplicitPhysicalId(trust--resourceknownPhysicalId, else read the template's physical-name property) andnormalizeAwsTagsToCfn(re-shape any AWS tag list —{Key,Value}/{TagKey,TagValue}/ map / lowercase — into the canonical CFnTagsshape, strippingaws:-prefixed entries so a CDK-deployed resource's reserved tags never fire false-positive drift). There is deliberately noaws:cdk:pathtag walk: AWS rejectsaws:-prefixed tag writes, so that tag never exists on a real resource and a walk keyed on it could not match (issue #1134, which removed the formerimport-tag-walk.tshelper + every provider's tag walk). Auto-mode import resolves any remaining ids from a same-named CloudFormation stack'sDescribeStackResources(issue #1128 / #1130), insrc/cli/commands/import.ts. - src/assets/file-asset-publisher.ts - S3 file upload with ZIP packaging support
- src/assets/docker-asset-publisher.ts - ECR Docker image build & push
- src/assets/asset-storage.ts - cdkd-owned asset storage (issue #1002 PR 1): naming helpers (defaults
cdkd-assets-{acct}-{region}/cdkd-container-assets-{acct}-{region}; issue #1011 addscdkd bootstrap --asset-bucket/--container-repocustom-name overrides, validated pre-AWS-call, marker-carried, differing-names re-bootstrap hard-errorsASSET_STORAGE_NAME_CONFLICT), the per-region bootstrap marker ats3://{stateBucket}/cdkd-bootstrap/{region}.json(ensureAssetStoragecreates the asset bucket + IMMUTABLE-tag ECR repo + marker-last fromcdkd bootstrap;--no-assetsopts out; owned-elsewhere buckets are hard-refused and every probe passesExpectedBucketOwner), and the deploy-timeAssetModeResolver(marker absent → legacy mode, byte-identical + onecdk gc-hazard info line per legacy region naming thecdkd bootstrap --region <r>fix; present →cdkd-assetsmode with bucket/repo existence verification, hard error on missing/malformed — never silent fallback;autoCreate(issue #1007, deploy-only, not under --dry-run) auto-creates bucket+repo+marker via the sameensureAssetStorageon first deploy into an un-opted-in region — confirm-gated (--yes/non-TTY auto-approve), decline/failure falls back to legacy + warning, opt out via--no-auto-asset-storage/context.cdkd.autoAssetStorage: false;useCdkBootstrapAssetsopt pins legacy with no marker read + no notice,suppressLegacyNoticequietsdiff/import).cdkd state infolists opted-in regions. The teardown counterpart issrc/cli/commands/bootstrap-destroy.ts(cdkd bootstrap --destroy, issue #1010 — asset bucket emptied+deleted → ECR repo force-deleted → marker deleted LAST; names from the marker; reference-scan refusal unless--force;--include-state-bucketadds the state bucket with stack-state / other-region-marker refusals). Design:docs/design/1002-cdkd-asset-storage.md. - src/assets/asset-redirect.ts - The cdkd-assets-mode wiring (issue #1002 PR 2):
buildAssetRedirectMap(destination-driven §6 mapping table from a stack's*.assets.json— only default-bootstrap-shapedcdk-[a-z0-9]+-(container-)?assets-{acct}-{region}destinations for the deploy account+region redirect; custom names / cross-region destinations stay verbatim per §8),rewriteTemplateAssetReferences(boundary-aware §7 deep rewrite over plain strings +Fn::Subtemplate strings + folded pseudo-parameter-onlyFn::Joinruns),findUnrewrittenAssetReferences(the deploy engine's §7-step-3 post-resolution audit viaDeployEngineOptions.assetRedirect— a surviving CDK-bootstrap reference fails the resource before provisioning),redirectFileAsset/redirectDockerAsset(publish-time redirection consumed byAssetPublisher.addAssetsToGraph({redirect})— the SAME table as the rewrite so they cannot diverge),createAssetRedirectResolver(lazy STS + marker gate fordiff/import),loadPublishableAssetManifest(asset-less stacks stay byte-identical). Rewrite call sites:deploy.ts(top-level),NestedStackProvider.readChildTemplateviaNestedStackProviderContext.assetRedirect,diff-recursive.ts'sbuildDiffTree,import.ts(top-level + recursive CFn-migration child walk);synth/exportunrewritten by design (§7.1).--use-cdk-bootstrap-assets(deploy/diff/import/publish-assets) orcdk.json context.cdkd.useCdkBootstrapAssetspins legacy. Integ:tests/integration/asset-migration/. - src/assets/docker-build.ts - Shared
docker buildinvocation reused bydocker-asset-publisher.ts(ECR publish path),src/local/docker-image-builder.ts(cdkd local invokecontainer Lambda path), andsrc/local/ecs-task-runner.ts(ECS run-taskContainerImage.fromAssetpath). Streams output viarunDockerStreaming(noexecFilemaxBufferceiling — fixes silent kills on# syntax=docker/dockerfile:1Dockerfiles where BuildKit progress + frontend pull exceeds the prior 50 MB cap). SetsBUILDX_NO_DEFAULT_ATTESTATIONS=1in the build env (matches CDK CLI'scdk-assets-lib). Full BuildKit flag set forwarded from the CDKDockerImageSourceschema (--build-context/--secret/--ssh/--network/--cache-from/--cache-to/--no-cache/--platform). Supports bothdirectoryandexecutablesource modes (the latter runs a user-supplied build script and reads the image tag from its stdout).Object.entries-stable build-arg order preserved (load-bearing for layer-cache stability). Parameterized error wrapping so each consumer threads its own typed error class. - src/types/assembly.ts - Cloud Assembly types (AssemblyManifest, MissingContext, etc.)
- src/types/rollback-journal.ts - Rollback-journal types + parser (issue #1183). Defines
RollbackJournal/RollbackJournalSegment/RollbackSegmentReason, theROLLBACK_JOURNAL_VERSIONconstant,parseRollbackJournal(JSON parse + validation), andUnknownRollbackJournalVersionError. The journal is a sibling ofstate.json({prefix}/{stackName}/{region}/rollback-journal.json), deliberately NOT part of the state schema — its ownjournalVersion(starting at 1), noStackState.versionbump. Read/written viaS3StateBackend.{load,appendSegment,popSegment,delete}RollbackJournal;deleteStatesweeps the key socdkd destroycleans it up. - src/provisioning/register-providers.ts - Shared provider registration (called from deploy.ts and destroy.ts)
- src/provisioning/data-delete-intent.ts - Shared destroy data-guard intent helpers (issue #1340):
hasCdkAutoDeleteTag(properties, tagKey)/isTruthyCfnBoolean(value)plus the CDK tag-key constantsS3_AUTO_DELETE_OBJECTS_TAG(aws-cdk:auto-delete-objects, stamped byautoDeleteObjects: true) andECR_AUTO_DELETE_IMAGES_TAG(aws-cdk:auto-delete-images). Consumed byS3BucketProvider.delete(auto-empty of a non-empty bucket only with the tag /DeleteContext.forceDataDelete),S3DirectoryBucketProvider.delete(issue #1344 — same gate; no CDK opt-in sugar exists for directory buckets, so plain destroy of a non-empty one fails with a manual-empty remediation), andECRProvider.delete(force: trueonly withEmptyOnDelete: true, the tag, orforceDataDelete) — without an opt-in the AWS not-empty error surfaces like CloudFormation DELETE_FAILED.DeleteContext.forceDataDelete(src/provisioning/region-check.ts) is set ONLY by the deploy engine's replacement/recreate delete sites under--force-stateful-recreation. See the "Destroy data guards" section in docs/cli-reference.md and the DeleteContext contract note in .claude/rules/providers.md. - src/provisioning/final-snapshot.ts -
DeletionPolicy/UpdateReplacePolicy: Snapshotsupport (issues #1352 / #1353 / #1354):ATOMIC_FINAL_SNAPSHOT_TYPES(RDS DBInstance / DBCluster, Neptune / DocDB clusters, ElastiCache CacheCluster — the delete call sites generatebuildFinalSnapshotIdentifier(physicalId, resourceType)and thread it viaDeleteContext.finalSnapshotIdentifier; each provider flips its delete fromSkipFinalSnapshot: trueto the API's atomic final-snapshot form; ONLY on the SDK route — a cc-api-routed atomic type is refused andCloudControlProvider.deletefail-closes on the field),PRE_DELETE_SNAPSHOT_TYPES+createPreDeleteFinalSnapshotdispatcher (all CC-routed:AWS::EC2::Volumevia EC2CreateSnapshottaggedcdkd:final-snapshot-of;AWS::Redshift::ClusterviaCreateClusterSnapshot;AWS::ElastiCache::ReplicationGroupvia ElastiCacheCreateSnapshot— each waited to ready, idempotent reuse via the tag / thefinalSnapshotNamePrefixname prefix across delete re-runs),unsupportedFinalSnapshotError/ccRoutedFinalSnapshotErrorrefusals, and (issue #1366)finalSnapshotMechanism(type, route)/refusesFinalSnapshot(type, route)— the mechanism matrix as a PURE function, so the executor that ACTS on it and thecdkd rollbackplan preview that DESCRIBES it read one source (issue #1368 extends that to the preview's STATE effect: a refused Snapshot delete no longer unwinds the record, since the next-older segment is classified against it). The two type sets are DISJOINT by construction —finalSnapshotMechanismtests the atomic set first, so a type in both would silently take the atomic arm and never reach the pre-delete snapshot; pinned infinal-snapshot.test.tsalongside the union-equals-the-CFn-documented-list fence (re-homed there from the deletedsupportsFinalSnapshotpredicate, #1368). Consumed by the deploy engine (prepareFinalSnapshotForDelete— the shared gate for the DELETE branch AND the four replacement / recreate delete sites),destroy-runner.ts, androllback-executor.ts— the latter twice:rollbackFinalSnapshotIdfor the delete-of-the-NEW-resource underUpdateReplacePolicy(honors only the atomic SDK-routed shape, plain-deletes otherwise — scope decision on #1354), andprepareCreateRollbackFinalSnapshotfor a rolled-back CREATE underDeletionPolicy(the FULL matrix, refusing what it cannot snapshot — issue #1358). The engine's clients come fromDeployEngineOptions.finalSnapshotClients(stack-region-pinnedAwsClients, structurally aPreDeleteSnapshotClients), threaded on toRollbackExecutorContext.finalSnapshotClients;--skip-final-snapshot(deploy / destroy / state destroy / rollback,skipFinalSnapshotOptioninsrc/cli/options.ts— deliberately NOT in the shareddestroyOptionsarraycdkd orphanconsumes) is the explicit data-loss opt-out. - src/provisioning/emr-configuration.ts - Shared CFn -> SDK shape converters for the
AWS::EMR::*nested config blobs whose CFn key spelling diverges from@aws-sdk/client-emr(issue #1383):toSdkConfigurations(renamesConfiguration.ConfigurationProperties-> the SDK'sPropertiesat EVERYConfigurationsnesting level),toSdkStepConfigs(HadoopJarStepConfig.StepProperties->Properties), andtoSdkInstanceTypeConfigs(per-instance-type nestedConfigurations). Both are pure key renames — the VALUE shapes already match (Record<string,string>/KeyValue[], verified against the live CFn registry schema) — but the AWS SDK v3 serializer drops unknown members, so before the conversion every EMR application configuration (spark-defaults / hive-site / yarn-site ...) silently vanished while cdkd reported success. Consumed byEMRClusterProvider(top-levelConfigurations/Steps, per-groupConfigurations, per-fleetInstanceTypeConfigs),EMRInstanceGroupConfigProvider(create), andEMRInstanceFleetConfigProvider(create + theModifyInstanceFleetupdate). No inverse is needed:Configurations/Steps/InstanceTypeConfigsare all declared inEMRClusterProvider.getDriftUnknownPathsand neither instance provider implementsreadCurrentState. Non-object / non-array inputs (an unresolved intrinsic) pass through untouched so AWS surfaces the real validation error. TheAWS::EMR::*types are NOT yet inNESTED_KEY_TARGETS(scripts/gen-nested-key-coverage.ts) — critic target expansion is tracked in issue #1393. - src/provisioning/ec2-termination-protection.ts - Shared
--remove-protectionhelper forAWS::EC2::Instance:disableInstanceApiTermination()(flipDisableApiTerminationoff, idempotent, errors swallowed at debug),isTerminationProtectionPropagationError()(matches the "may not be terminated. Modify its disableApiTermination" 400 from bothTerminateInstancesand the Cloud ControlDeleteResourcewrapper), andTERMINATION_PROTECTION_MAX_ATTEMPTS. Used byEC2Provider.deleteInstance(SDK path) andCloudControlProvider.delete(CC-API path — an instance routes through Cloud Control whenever its template trips the #614 silent-drop routing) so--remove-protectionworks regardless of which delete path the instance takes; the modify WRITE lags the delete READ, so both callers flip-off + retry the delete to close the propagation window. ALSO used byASGProvider.delete(issue #796): anAWS::AutoScaling::AutoScalingGroupwhose launch template setsDisableApiTermination: truelaunches instances that survive the group'sDeleteAutoScalingGroup(ForceDelete: true)(ASG-level DeletionProtection + ForceDelete governs only the group + scale-in protection, not EC2-level termination protection), so under--remove-protectionthe provider enumerates the group's current instances and flips each one'sDisableApiTerminationoff before the force delete — the ASG's own async terminate loop then absorbs the modify-WRITE propagation lag, so no per-instance delete retry is needed there. An ASG can ALSO route via Cloud Control when its template sets a silent-drop property such asAvailabilityZoneIds(#614 routing) — Cloud Control'sDeleteResourcecannotForceDeletea protected ASG or clear its protection, soCloudControlProvider.deletedetectsremoveProtection === true && resourceType === 'AWS::AutoScaling::AutoScalingGroup'and delegates tonew ASGProvider().delete(...)(the single source of truth for protected-ASG deletion), keeping the SDK and CC routing paths behaviourally identical (issue #798; CDK's L2 emitsavailabilityZonesnames notAvailabilityZoneIds, so this CC path only fires for hand-written L1 / imported templates). - src/provisioning/unsupported-types.ts + unsupported-types.generated.ts - Pre-flight unsupported-type rejection. The
.generated.tsships the provider-coverage Tier 3 set (ProvisioningType: NON_PROVISIONABLE) into the runtime, codegen'd fromdocs/_generated/provider-coverage.jsonbyscripts/gen-unsupported-types.ts(vp run gen:unsupported-types; CI fails on drift). The hand-written.tsaddsisNonProvisionable()+unsupportedTypeIssueUrl(); both are consulted byCloudControlProvider.isSupportedResourceType(rejects Tier 3) andProviderRegistry.validateResourceTypes(per-type error + issue link). The--allow-unsupported-typesescape hatch routes named types through Cloud Control viaProviderRegistry.allowUnsupportedTypes(). - src/provisioning/property-coverage.ts + property-coverage.generated.ts - Pre-flight property-level rejection (parallel to unsupported-types but at top-level CFn property granularity). The
.generated.tsships per-Tier-1-type{ handled, silentDrop }records, codegen'd fromtests/fixtures/cfn-schemas/*.json+ each SDK provider'shandledProperties/unhandledByDesigndeclarations byscripts/gen-property-coverage.ts(vp run gen:property-coverage; CI fails on drift; the codegen parses provider sources via the TypeScript Compiler API so nodist/bootstrap is needed). The hand-written.tsaddsgetPropertyCoverage()+findSilentDropProperties()+unsupportedPropertyIssueUrl(); all are consulted byProviderRegistry.validateResourceProperties(per-resource per-property error + 1-click GitHub issue link + dedup'd re-run command). The--allow-unsupported-propertiesescape hatch (deploy only) routes named<Type>:<Prop>entries past the reject viaProviderRegistry.allowUnsupportedProperties(). Tier 2 (Cloud Control) types are intentionally NOT in the generated map — CC forwards the full property map to AWS, so no write-side silent drop is possible. - scripts/gen-enrichment-coverage.ts + docs/_generated/enrichment-coverage.{json,md} - CC-API enrichment-coverage completeness matrix + CI critic (
vp run gen:enrichment-coverage/vp run audit:enrichment-coverage:check; CI fails on drift AND on a pure-CC latent gap). Makes the enrichment-gap bug class (#844 / #864 / #865 / #866) non-regressing. The generator parses theenrichResourceAttributesswitch insrc/provisioning/cloud-control-provider.tsvia the TypeScript Compiler API (per-caseenriched['Attr']keys, flat-keys likeEndpoint.Addressmatched to the nested readOnly propEndpoint), cross-references each type'sreadOnlyPropertiesfrom the cached CFn schema fixtures (tests/fixtures/cfn-schemas/*.json), and classifies each intoenriched/no-computed-attr/sdk-fallback-gap(gap on an SDK-backed type — only exposed on the #614 CC-fallback path, informational) /unenriched-computed(gap on a pure-CC type with no SDK provider — the real bug class). The--checkcritic hard-fails ONLY onunenriched-computed. A readOnly prop that is the type'sprimaryIdentifieris auto-classified not-a-gap (the resolver's physicalId fallback resolves it);scripts/refresh-cfn-schemas.mjscapturesprimaryIdentifierinto the fixtures for this. The seedENRICHMENT_ALLOW_LISTcarves outAWS::MSK::Cluster(Arn == primaryIdentifier) +AWS::Elasticsearch::Domain(Tier-3 non-provisionable). Classifier is unit-tested (tests/unit/scripts/gen-enrichment-coverage.test.ts). NO AWS integ (pure static analysis / codegen). - scripts/gen-sdk-attr-coverage.ts + docs/_generated/sdk-attr-coverage.{json,md} - SDK-provider ARN/URL attribute-coverage matrix + CI critic (
vp run gen:sdk-attr-coverage/vp run audit:sdk-attr-coverage:check; CI fails on drift AND on an unresolvable Arn/Url attribute). The SDK-provider-side sibling ofgen-enrichment-coverage.ts(which only audits the CC provider'senrichResourceAttributesswitch). Makes the #1179 GetAtt-key bug class (SDKcreate()/update()records an ARN under a non-CFn key) non-regressing. Output / cross-resourceFn::GetAttreads the cachedresource.attributes[<CFnName>]inIntrinsicFunctionResolver.constructAttribute(which never calls a provider'sgetAttribute), so an ARN stored under the wrong key is missed and — for a*Arn/*Urlname — HARD-FAILS the resolver's shape guard (#1179 storedArnnotAgentRuntimeArn, breaking aCfnOutput). The generator parses (a) each provider's create/update attribute-object keys (collectStoredAttributeKeys, object-literal + element-access-assignment keys — acase '<Attr>':label ingetAttributeis deliberately NOT collected, so a provider handling the ARN only ingetAttributeis still flagged), (b) thehandledPropertiesmaps (which types each provider serves), and (c) the set of typesconstructAttributereferences, all via the TS Compiler API. AnArn/Urlread-only attribute (minusprimaryIdentifier) is agapiff it is NEITHER cached by the provider NOR the type isconstructAttribute-handled NOR allow-listed — scoped toArn/Urlbecause those are the ONLY suffixes the resolver's guard hard-fails on (non-ARN attrs warn-and-fallback and are legitimately left uncached). TheSDK_ATTR_ALLOW_LISTseedsAWS::SNS::Subscription.Arn(NOT-A-BUG: physicalId IS the subscription ARN, guard fallback resolves it). It initially also carriedAWS::Lambda::EventSourceMapping.EventSourceMappingArnas a KNOWN GAP tracked in #1190; that real gap was fixed (the provider now caches the ARN under its CFn name) so the entry was removed and the critic now verifies it stays cached. Classifier + parsers are unit-tested (tests/unit/scripts/gen-sdk-attr-coverage.test.ts, incl. a real-repo coverage floor). NO AWS integ (pure static analysis / codegen). Filed as issue #1187. - scripts/gen-update-wrap-coverage.ts + docs/_generated/update-wrap-coverage.{json,md} - SDK-provider
update()error-wrapping coverage matrix + CI critic (vp run gen:update-wrap-coverage/vp run audit:update-wrap-coverage:check; CI fails on drift AND on a non-allow-listed gap). Third member of the codegen'd-critic family alongsidegen-enrichment-coverage.ts(CC attribute enrichment) andgen-sdk-attr-coverage.ts(SDK ARN/URL attribute keys). Makes the "update()leaves AWS SDK errors unwrapped" class non-regressing — a defect found TWICE by review and never by a test (#1263 -> PR #1265LambdaUrlProvider; #1267 -> PR #1268 EventBridge bus / SNS topic / Lambda event-source / Logs log-group). Scanssrc/provisioning/providers/*.tsPLUSsrc/provisioning/cloud-control-provider.ts(the widest-coverage provider, which lives one directory up and was initially missed). The analysis is interprocedural within one class: it walks from the publicupdate()carrying aprotectedflag, sets the flag inside anytrywhosecatchraises aProvisioningError— counting ALL THREE spellings: the literalthrow new ProvisioningError(...), the throw-form FACTORYthrow this.wrapError(...), and the STATEMENT-formthis.handleError(error, ...)with nothrowkeyword at all (CloudControlProvider's shape; missing it made that clause look like a swallow and silently disabled the pass-through check for the widest-coverage provider in the repo) — and also sets it for a genuinely SWALLOWINGcatch(log-and-continue, where no raw error can propagate; a conditional re-throw, areturn Promise.reject(...), or a statement call to anever-returning helper are NOT swallows). Aneverreturn alone does not make a method a wrap factory, and anew ProvisioningErrorburied in a nested closure the method never returns does not either, followsthis.x()calls into the class's own members inheriting the flag (arrow-function class PROPERTIES included), and reports any.send(...)reached with the flag clear. An unresolvablethis.x()callee is recorded and surfaces asunresolved-calleerather than silently reportingno-aws— sends behind it are unobservable, so a confident green there would be a false clean. Following delegation is load-bearing, not a nicety: both real shapes depend on it (the #1268 boundary-wrapper — wrap inupdate(), sends in a privateapplyUpdate()— and the inverses3-tablesshape — bareupdate(), wrapping inside each helper), and a hand-rolled brace-matching grep lacking it produced a confirmed false positive. The critic ALSO enforces the paired invariant PR #1268 established: a wrappingcatchthat can capture a control-flow typed error — raised asthrowOR asreturn Promise.reject(new ...)(9 real sites across ec2 / ecs / apigateway / lambda-layer, which a throw-only match left silently uncovered) — MUST re-throw the CAUGHT BINDING (if (error instanceof CdkdError) throw error;) — a positiveinstanceoftest that throws anything else is the #1268 defect itself, and the negatedif (!(error instanceof CdkdError)) throw error;is the inverse shape, so both are rejected.CONTROL_FLOW_THROW_CLASSES(blocking) holdsResourceUpdateNotSupportedError— which changes BEHAVIOR when swallowed (the deploy engine matches it by class to fall back to replacement) — plusProvisioningError, promoted by #1272 once the last re-labelling site was fixed (swallowing one is cosmetic, but every site is clean so blocking keeps it that way; adding a class here is only safe when the tree is already free of it). It stays a SEPARATE set fromTYPED_PASSTHROUGH_CLASSES(accepted guards), where being generous is always safe. The pass-through may live in a DELEGATED throw-helper rather than lexically in the catch (CloudControlProvider'shandleError), and a THROW-form factory mayreturnthe typed error for the caller to throw (lambda-microvm-image'swrapError) — both count; areturninside a STATEMENT-form helper does not, since that swallows. Buckets:wrapped/no-aws/gap(blocks) /unguarded-wrap(blocks) /allow-listed(a real gap deliberately not blocking, kept VISIBLE rather than relabelledwrapped) /unresolved-callee(visible, non-blocking).UPDATE_WRAP_ALLOW_LISTis keyedClass#methodviaallowKey()so an entry for one method cannot silence a NEW gap elsewhere in the same class; it is EMPTY as of #1270 — the 5 gaps found on introduction (EC2 / ELBv2 / Firehose / Kinesis stream-consumer / S3 Tables) were all fixed with the #1268 boundary-wrapper shape, and removing their entries is what made the critic verify those fixes and block a re-regression. Unit tests (tests/unit/scripts/gen-update-wrap-coverage.test.ts) cover each shape plus real-repo floors — including the EXACTno-awsclass set pinned by name (a>= 1floor would let a class silently drop intono-aws, the shape a lost delegation edge produces, while the aggregatewrappedfloor absorbed it), assertions thatunresolved-calleeANDallow-listedare both zero, a fence that the five #1263/#1267-fixed providers still classifywrapped, and a stale-allow-list-entry check. NO AWS integ (pure static analysis / codegen). Filed as issue #1269. - scripts/gen-nested-key-coverage.ts + docs/_generated/nested-key-coverage.{json,md} - Nested CFn->SDK key-divergence coverage matrix + CI critic (
vp run gen:nested-key-coverage/vp run audit:nested-key-coverage:check; CI fails on drift AND on a non-allow-listed divergence). Fourth member of the codegen'd-critic family (issue #1373). Makes the write-side nested-key silent-drop class non-regressing — the AWS SDK v3 serializer drops unknown keys, so an SDK provider forwarding a nested CFn config blob silently loses every key whose spelling it does not convert;property-coveragecompares TOP-LEVEL names only, and the class recurred 4 times before tooling (#1165/#1167 ECS casing, #1160 API GW v2, #1304MetricTimeZone, #1370 CloudFront x5). Per declared target (NESTED_KEY_TARGETS: CloudFront Distribution, CloudWatch AnomalyDetector, API GW v2 x5, ECS Service / TaskDefinition, CodeBuild Project, S3 Bucket), the critic diffs the fixture'snestedPropertiescapture (added toscripts/refresh-cfn-schemas.mjsby this issue — per-top-level-property nested names,$ref-resolved + cycle-guarded) for the provider's OWNhandledPropertiestop-levels against the SDK client model member names (node_modules/<pkg>/dist-types/models/*.d.tsPropertySignatures via the TS Compiler API), with the provider's AST-level string literals (comments excluded) as evidence of explicit per-key handling. Key style is per-target (exactfor PascalCase SDK models,lower-firstfor camelCase ECS). Buckets:same-spelling/provider-handled/allow-listed(rationale'd pass-throughs, e.g. the 3 legacy pre-2012 CloudFront members) /case-divergence(case-insensitive SDK near-miss — the highest-signal bucket, blocks CI) /no-sdk-member(blocks CI) /no-write-evidence(blocks CI, issue #1432 — see the WRITE-EVIDENCE pass below). Parser-regression floors: per-targetminNestedKeys+MIN_SDK_MEMBERS_PER_CLIENT+MIN_WRITTEN_MEMBERS_PER_PROVIDER, so a broken parse fails loudly instead of passing vacuously; stale allow-list entries fail in BOTH modes (an SDK bump that makes an allow-listed key reachable forces the entry's removal). The first run found TWO live bugs fixed in the same PR: CloudFrontOriginCustomHeaders(never renamed to the SDK'sCustomHeaders— origin custom headers silently dropped on create AND actively wiped on update by the required-field fill) and ECS TaskDefinitionS3FilesVolumeConfiguration(SDK member is the irregular all-lowercase-prefixs3filesVolumeConfiguration, unreachable by the mechanical first-letter flip — the whole S3 Files volume block was dropped); plus 8 keys resolved by bumping@aws-sdk/client-cloudfront/client-ecs(members newer than the pinned SDK). Since issue #1378 a SHAPE pass rides the same run: the fixtures additionally capturedefinitionShapes(per CFn definition, member -> terminal type kind,$ref-resolved; the top-level block under the reserved#topkey), the SDK side parses full interfaces with member type kinds (collectSdkInterfaces+wrapperInterfaceNames— aQuantity-bearing interface is a{Quantity, Items}wrapper), and two CI-blocking shape buckets cover what the key pass is structurally blind to (the spelling exists SOMEWHERE in the SDK model):array-vs-wrapper(a CFn bare-array member whose same-spelled SDK members are all wrapper refs — mechanizes the previously hand-maintained CloudFrontQUANTITY_ITEM_FIELDSclass, so a NEW array member AWS adds flags until wrapped) anddefinition-member-missing(a CFn definition's member same-spelling an SDK member globally but missing from the same-named SDK interface — theCachedMethodssibling-vs-nested /GeoRestriction.Locations/ legacyS3Originclass). Shape evidence uses the DOT-SEGMENT-EXPANDED literal set ('ForwardedValues.Headers'names both segments); the key pass keeps the strict set. Non-blocking visibility:ambiguousshapes + unmatched-definition counts. First shape audit found no live bug — the QUANTITY_ITEM_FIELDS family (13 wrapper re-shapings) and CachedMethods all classify provider-handled, with legacyS3Originthe one new allow-list entry (invisible to the key pass because the StreamingDistribution API still carries a same-spelled member). The #1378 rider also gaverefresh-cfn-schemas.mjsa--help/ unknown-flag guard (an unrecognized flag previously fell through to a silent FULL ~135-type re-fetch). Issue #1430 addedAWS::S3::Bucket(115 nested keys, second only to CloudFront Distribution's 121) — the type had been forwarding a dozen nested blobs without a critic since before the #1388 / #1424 lifecycle defects were hand-fixed in PR #1426 — and its first run foundNotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabledbroken in BOTH directions: CFn carries a required boolean while the SDK's block is an EMPTY structure whose PRESENCE enables delivery, soEventBridgeEnabled: falsesilently ENABLED notifications on the write side andreadCurrentStatereturned the SDK{}shape the CFn-shaped state baseline could never match. Its three allow-list entries (TableName/TableArn/TableNamespace) are the FIRST real instance of the unreachable-definition false positiveclassifyTargetShapesdocuments — reachable only from silent-drop, Cloud-Control-routed top-levels, so no SDK forwarding path exists to drop them. Issue #1432 added a third, OPT-IN pass — WRITE-EVIDENCE — closing the fact thatsame-spellingis the critic's SILENT bucket and is only sound for a provider that FORWARDS a blob: one that builds a FRESH SDK object naming each member drops any member it never names, spelling agreement notwithstanding (AWS::CodeBuild::ProjectBuildBatchConfig.BatchReportModestayed silent even with every occurrence of the SDK spellingbatchReportModerenamed away, which is what proved the gap structural). A target settingfreshObjectMapper: truerequires each would-be-same-spellingkey to ALSO appear as a WRITTEN SDK member name (collectWrittenMemberNames: object-literal property, shorthand property, or assignment target — a READ deliberately does not count, which is what scopes the evidence to the CFn->SDK direction), elseno-write-evidence.WRITE_EVIDENCE_EXCLUDED_FUNCTION_PREFIXESskips reverse-map bodies by word-boundary PREFIX, because for anexact-style target the reverse map's CFn-spelled WRITE would otherwise vouch for the forward mapper — #1393 item 2 one bucket over (measured withdrawal: 8 names froms3-bucket-provider.ts, 71 fromcodebuild-provider.ts, 42 fromecs-provider.ts; the ECS number is 0 under an exact-name match, which is why the prefix form is load-bearing rather than cosmetic). The pass's own BOUND is documented rather than papered over: evidence is a flat per-FILE name set and the audited unit is a key NAME not a path, so a member written anywhere vouches for every key of that spelling — 11 of CodeBuild's 55 same-spelling keys have >1 write site andBuildBatchConfig.ServiceRolestays silent when dropped, so the pass fences the 44 uniquely-named members; moving the key model to paths is issue #1448. The pass is opt-in because the opt-in set was MEASURED, not predicted: no-write-evidence counts are CodeBuild 0/55, CloudWatch AnomalyDetector 12/20, API GW v2 13/13, S3 19/89, ECS TaskDefinition 22/107, ECS Service 37/48, CloudFront 70/112 — and those 173 are the pass's blind spot rather than silent drops, a GENERIC key converter delivering a whole sub-blob with no member to find (ECSProvider.convertLinuxParametersisreturn pascalToCamelCaseKeys(config)). Only CodeBuild opts in today (0 findings, so the #1386 defect becomes non-regressing for free); following a whole-blob hand-off into a generic converter, the taint walkgen-handled-property-wiringalready does one level up, is issue #1445. Unit tests (tests/unit/scripts/gen-nested-key-coverage.test.ts) cover each bucket, the fixture-capture walker, real-repo floors + fences (the #1370/#1373/#1304-fixed keys stayprovider-handled), and REAL-CODE regression probes per the repo's checker rules (the real CloudFront source with a real conversion stripped must flag, named key + SDK near-miss; and for the write-evidence pass, deleting ONLY the forwardbatchReportMode:write from the realcodebuild-provider.ts— leavingreadCurrentState's reverse map intact — must flag, while the SAME regression with the opt-in removed must stay silent). NO AWS integ for the critic itself (offline static analysis; fixture re-capture needscloudformation:DescribeType). - scripts/gen-handled-property-wiring.ts + docs/_generated/handled-property-wiring.{json,md} -
handledPropertiesWIRING coverage matrix + CI critic (vp run gen:handled-property-wiring/vp run audit:handled-property-wiring:check; CI fails on drift AND on a non-allow-listed gap). Fifth member of the codegen'd-critic family (issue #1404). Closes the gap its siblings structurally cannot see:gen-property-coverage.tsverifies every CFn property is ACCOUNTED FOR (declared inhandledPropertiesorunhandledByDesign) andgen-nested-key-coverage.tsaudits spellings INSIDE a forwarded blob, but neither checks that ahandledPropertiesentry is actually WIRED.ECRProviderdeclaredImageTagMutabilityExclusionFiltershandled while the property appeared on NO API call, so the pre-flight passed on the declaration alone and the value silently vanished (issue #1392, fixed in PR #1406) — a FALSE handled claim, the exact thing the declaration system exists to prevent. For every declared property the critic requires evidence that the provider CONSUMES it, in one of four AST shapes:element-read(properties['X']),property-read(properties.X),destructure, andtable-loop(properties[k]wherekiterates a literal name list — inline array, enclosing-scopeconst, orObject.entries(TABLE)), plus an orthogonaldelegatedtag when the read happens in a callable reached by a call edge. Two rules keeptable-loopfrom becoming a rubber stamp, since one syntactic site there credits N properties at once (43 tagged today across 5 classes, 29 of them withtable-loopas their ONLY evidence — GlueJobProvider 16, SQSQueueProvider 13). The loop body must DELIVER, not merely compare:EC2Provider.updateSubnet'sfor (const createOnly of ['VpcId', ...]) { const next = properties[k]; ... if (next !== prev) throw }is a change GUARD, and crediting it smuggled the diff-is-not-delivery disguise back in one level up, multiplied by the table — so a table read counts only when someproperties[k]in that body escapes comparison (following oneconsthop, and seeing throughJSON.stringify/typeof/.length/ truthiness). The rule withdrew the tag from 46 properties across 8 classes on introduction (EC2 / AppSync / EFS / Lambda / RDSDBProxy / Firehose immutability + change-detection guards), NONE of which became a gap — all are also read individually;RDSDBProxyProvidershows the discrimination cleanly, its immutable-field loop losing the credit while itsmutableFieldsloop (input[sdkKey] = properties[key]) keeps it. And the table is resolved LEXICALLY — from the loop outward through enclosing blocks, function bodies, then module scope — because a FILE-wide pool let a table local to one class's method vouch for a DIFFERENT class in the same file and let two same-named tables override each other last-wins (glue-provider.tsreally does declareresultx12,outx5,toAddx3). Evidence is CLASS-SCOPED via a taint walk seeded from each method's desired-state parameter and propagated only through calls that pass the bag WHOLE — so a sibling class in the same file, a comment, agetDriftUnknownPathsentry, thehandledPropertiesdeclaration itself, and areadCurrentStatewrite-back all fail to vouch for a property (each pinned by a test). Two strictness decisions were forced by the real tree rather than by fixtures: a whole-bag forward (this.helper(properties)) does NOT blanket-excuse un-read declarations — the first draft's blanket excuse silenced the very #1392 property viaECRProvider'shasCdkAutoDeleteTag(properties)call indelete(), and measured across the tree the excuse rescued 0 of 1063 properties (the count before #1411 / #1412 retired two declarations), so blind spots are now recorded for VISIBILITY only (isInertWholeBagUseadditionally exempts by shape a result that only feeds a comparison or a.lengthmeasurement, e.g.JSON.stringify(a) === JSON.stringify(b)— but ONLY on the un-resolvable branch: a resolvable callee is always walked, since skipping it dropped its reads while recording nothing); and a read ofpreviousPropertiesis NOT evidence, since a diff-only read proves change DETECTION, not delivery. That last exclusion is narrower than it looks and the in-code JSDoc says so: it does NOT close the "diffs it then forgets to send it" disguise for a singleelement-read(the desired-side half ofproperties['X'] !== previousProperties['X']still clears the property), only for helpers reached with the previous bag alone; the TABLE-loop case IS closed, per the delivery rule above. Each wired property also recordsseededBy, the class member(s) whose walk produced the evidence, so a propertywiredonly from a non-delivery member such asreadCurrentState()is visible rather than silently green (0 today, fenced by a test).HANDLED_WIRING_ALLOW_LISTis keyed by PROPERTY (not class) so a class allow-listed for one property still blocks CI on a new un-wired sibling, and carries the same KNOWN-GAP-vs-NOT-A-BUG split asgen-sdk-attr-coverage.ts; stale entries fail in both modes, so wiring a property forces its entry's removal. Coverage floors are per SHAPE (not just a grand total) — 84 classes / 1061 declared properties, withproperty-read+destructurepinned=== 0(no real-tree user today; the recognizers are proven synthetically, so a future user cannot regress them silently). The critic's FIRST real-tree run found two live gaps, seeded as KNOWN GAP entries and filed rather than fixed in the introducing PR:AWS::EC2::NatGateway.MaxDrainDurationSeconds(issue #1411) andAWS::Logs::LogGroup.ResourcePolicyDocument(issue #1412). BOTH are now fixed and their allow-list entries REMOVED: neither property can be delivered by its SDK provider (noCreateNatGatewaymember and no NAT gateway modify API for the first; an account-wideAWS::Logs::ResourcePolicywith noCreateLogGroupcounterpart for the second), so each moved tounhandledByDesign, which converts the invisible drop into the #614 Cloud Control auto-route. What remains allow-listed isIAMAccessKeyProvider#SerialandNestedStackProvider#TemplateURL, both rationale'd NOT-A-BUG entries, and the exact remaining set is pinned by name in the test. Unit tests (tests/unit/scripts/gen-handled-property-wiring.test.ts) carry the shape coverage, the floors, and REAL-CODE fail probes per the repo's checker rules — reverting the realecr-provider.tsto its pre-#1406 state must exit non-zero naming the property (a first probe that stripped only the lowercase-preads PASSED, because the survivingpreviousPropertiesread cleared it; that false clean is what drove thepreviousPropertiesexclusion, and both variants are now automated). The real-code probe set also covers the two table rules (strippingproperties['VpcId']from the realec2-provider.tsmust NOT leave the createOnly loop vouching for it; dropping one name from the real Glue / SQS tables must surface a gap; a class appended to the realglue-provider.tsmust not borrowbuildJobCommonFields's local table), the stale-allow-list verdict (injecting aproperties['Serial']read into the realiam-access-key-provider.tsmust report that entry stale — the probe was re-pointed there when the two KNOWN GAP entries retired), and theproperty-read/destructurerecognizers (rewriting the real ECR read into each shape must stay wired). The shipped--checkcommand itself is exercised viaspawnSyncagainst a scratch COPY ofsrc/provisioning/providerscarrying the injected regression (--providers-dir=test seam), so the exit code and failure text are covered without ever writing tosrc/. NO AWS integ (pure static analysis / codegen). - src/provisioning/describe-type.ts - Shared
cloudformation:DescribeTypeinvocation with THROTTLE-ONLY retry (issue #1236), consumed bywrite-only-properties.ts,create-only-properties.ts, andexport.ts's primary-identifier resolution (which passes its own injected CFn client via the optional second argument). DescribeType is throttled per-account and the #1182 create-only prefetch can burst through the limit at deploy start, so an on-critical-path lookup moments later (the write-only resolution during a CC-routed UPDATE) was reliably throttled — and the resolvers' graceful fallbacks turned that transient throttle into a real failure (droppedAWS::ECS::Service.VolumeConfigurations-> UpdateService 400, or a registry-only replacement classification).describeTypeWithThrottleRetrywraps the call inwithRetrywithisRetryable: isThrottlingError(name/$metadata-based, NOT message-based) and 4 retries (1s->2s->4s->8s, ~15s max sleep); non-throttle failures (missing IAM permission) rethrow immediately so the warn-and-fall-back path stays as fast as before.describeTypeRetryDelays.sleepis the test seam. Also exportshasNoRegistrySchema(resourceType)— the ONE list of types with no CloudFormation registry entry (Custom::*,AWS::CloudFormation::CustomResource, and theAWS::CDK::Metadatasynth sentinel), for which DescribeType can only fail. Both resolvers short-circuit on it, and the deploy engine's create-only prefetch filters the template's type set through it, so theAWS::CDK::Metadataresource present in EVERY synthesized template no longer burns a guaranteed-to-fail API call plus a misleading "Grant cloudformation:DescribeType" warning on every deploy. - src/provisioning/write-only-properties.ts - Write-only property resolution for Cloud Control UPDATE patches (issue #809).
getTopLevelWriteOnlyProperties(resourceType)resolves the type's registry-schemawriteOnlyPropertiesviacloudformation:DescribeType, reduced to top-level containing property names (nested/properties/Foo/Barstrips toFoo), short-circuiting to the empty set forhasNoRegistrySchematypes, cached per type in a module-level promise map for the deploy lifetime (only SUCCESSFUL lookups are cached; a DescribeType failure warns and falls back to an empty set for that update WITHOUT caching, so a transient throttle does not poison write-only re-inclusion for the rest of the deploy — a later update of the same type retries; a Schema-less response is a successful "no write-only props" lookup, warning-free). Consumed byCloudControlProvider.update, which strips these properties from the PREVIOUS side before patch generation so the patch always carriesaddops for write-only properties in the desired state — Cloud Control applies patches read-modify-write and read handlers cannot return write-only properties, so a write-only property absent from the patch document would be dropped on every UPDATE (e.g.AWS::ECS::Service.VolumeConfigurationshard-fails; other types lose config silently). Mirrors terraform-provider-awscc's prior-state clearing. DescribeType goes throughdescribe-type.ts's throttle-only retry (issue #1236) before the fallback fires.clearWriteOnlyPropertiesCache()is test-only. - src/provisioning/slow-cc-operation-timeouts.ts - Per-(resourceType, operation) wall-clock timeout floors (ms) for types whose async CREATE / UPDATE / DELETE routinely exceeds cdkd's generic deadlines (OpenSearch / Elasticsearch domains; Redshift / ElastiCache / RDS clusters — all 60 min).
slowCcOperationTimeoutMs(resourceType, operation)returns the floor or0(generic default applies). The SINGLE source of truth consulted by all three cap sites so the inner and outer budgets can never drift apart:CloudControlProvider.waitForOperation's internal poll cap (Math.max(MAX_WAIT_TIME_MS, floor)), the destroy-runner outer per-resource deadline, and the deploy-engine outer per-resource deadline (bothMath.max(providerMinTimeoutMs, floor, globalTimeoutMs)). Fixes theopensearch-domain-getattdestroy timeout (a domain delete runs 15-30 min but the flat CC cap was 15 min, sowaitForOperationthrewDELETE timeout after 900smid-delete). A--resource-timeout <TYPE>=<DURATION>override still wins at the outer sites. - src/provisioning/resource-timeout-registry.ts - Process-wide registry of the user's resolved
--resource-timeoutinput (issue #1280) — the SDK-provider analogue ofslow-cc-operation-timeouts.ts's inner-undercuts-outer fix, sourced from CLI input instead of hardcoded floors.setResolvedResourceTimeouts(opt)is seeded bydeploy.ts/destroy.ts/state.ts(state destroy) right aftervalidateResourceTimeouts(wiring pinned by a source-level test);resolvedResourceTimeoutMs(resourceType)resolves per-type override > explicit global >undefined(the compile-time 30m default deliberately does NOT leak in — only an explicit user value may lift an inner waiter's floor). Consumers:ECSProvider.settleService's--full-waitsteady-state waiter (capmax(600s, resolved)) andCloudFrontDistributionProvider.waitForDistributionStable's Deployed-wait budget (capmax(20min, resolved), issue #1282 — reached by the--full-waitcreate/update settle AND the delete path's API-required disable-then-wait), so--resource-timeout <TYPE>=<duration>actually reaches the inner waiters instead of only the outer deadline. - src/provisioning/create-only-properties.ts - Create-only (immutable) property resolution for REPLACEMENT detection — the read-side sibling of
write-only-properties.ts(samecloudformation:DescribeType+ per-type cache + graceful-degradation pattern).getCreateOnlyPropertyPaths(resourceType)resolves the type's registry-schemacreateOnlyPropertiesas FULL segment paths (schema-less types —Custom::*/AWS::CloudFormation::CustomResource/AWS::CDK::Metadata, per the sharedhasNoRegistrySchemapredicate indescribe-type.ts— skip the DescribeType lookup entirely and resolve to an empty list, since they have no registry schema and the lookup would always fail with a misleading warning; issue #1016), and the purecreateOnlyChangeRequiresReplacementcompares a changed top-level property at path granularity — a nested createOnly entry only forces replacement when the value AT that path changed (issue #960; unresolvable shapes and unresolved intrinsics stay conservative = replacement). Consumed byDiffCalculator.comparePropertiesas a fallback for any property the hand-authoredReplacementRulesRegistrydoes not explicitly classify (ReplacementRulesRegistry.isClassifiedgates it so a deliberateupdateablePropertiesdecision is never overridden) — so an immutable-property change on ANY type (not just the ~25 with a hand-written rule) is correctly classified as a replacement instead of an in-place UPDATE. The deploy engine's property-driven replacement path then applies the same--force-stateful-recreationstateful guard as--replace, so a template immutable-property change can no longer silently DELETE+CREATE a stateful resource's data. DescribeType goes throughdescribe-type.ts's throttle-only retry (issue #1236) before the fallback fires. The fallback is only as good as the registry schema: some types declare NOcreateOnlyPropertiesat all even though AWS rejects the update (AWS::EC2::Volume— live-verified 2026-08-03, issue #1356), so the fallback finds nothing and the change is misclassified as in-place. Such a type needs a hand-authoredReplacementRulesRegistryentry; when the type is data-bearing, add it toSTATEFUL_TYPESin the same change so the newly-reachable replacement path cannot silently destroy data.clearCreateOnlyPropertiesCache()is test-only. - src/types/ - Type definitions (config, state, resources, assembly, etc.)
- src/utils/ - Logger, ANSI color helpers (
colors.ts—green/yellow/red/cyan/gray/bold/diminline wrappers; kept in a separate module fromlogger.tsso test files thatvi.mock('../../../src/utils/logger.js', ...)don't accidentally strip color helpers and crash any code path that imports them), per-resource status-line formatter (resource-line.ts—formatResourceLine(op, logicalId, resourceType, verbOverride?)builds the shared<glyph> <id> (<type>) <verb>line printed bycdkd deploy/cdkd destroyfor created / updated / deleted; every successful op renders a check ✓ — never a cross ✗ — and is distinguished by COLOR not glyph (green created / yellow updated / green-check-plus-red-verb deleted), so no success line is ever mistaken for the red ✗ "Failed to delete" failure path;verbOverrideswaps the verb word, e.g.'updated (metadata)'), live progress renderer (multi-line in-flight task display), error handler (incl.normalizeAwsErrorfor AWS SDK v3 synthetic UnknownError → actionable HTTP-status-keyed messages), AWS client factory, AWS region resolver (aws-region-resolver.ts— caches bucket-region lookups viaGetBucketLocationso the state-bucket S3 client can be rebuilt for the bucket's actual region), state-bucket owner guard (expected-bucket-owner.ts—resolveExpectedBucketOwner(client)/expectedOwnerParam(client): STSGetCallerIdentityon the client's own credentials, spread asExpectedBucketOwnerinto EVERY state-bucket-family S3 call — S3StateBackend / LockManager / ExportIndexStore / bootstrap / state-migrate / state.ts raw reads / upload-cfn-template — so a predictable-name bucket pre-created in a foreign account is rejected by S3 itself (403) even when its policy ALLOWS this account; asset-storage has carried the same defense since #1002 PR 1; best-effort: test doubles / STS failure omit the header. Memoized in TWO layers (issue #1283): a per-clientWeakMapfast path, plus the STS call itself keyed by the resolved ACCESS KEY ID — an access key belongs to exactly one account, so the several clients cdkd builds from one credential chain in a deploy preflight (the bucket-existence probe, the sharedAwsClients.s3, the region-corrected rebuild) share ONE round trip, while an assumed-role session (its ownASIA…key) still resolves its own account — which is what keeps the cross-accountFn::GetStackOutputRoleArnpath correct.recordResolvedAccountId(client, accountId)seeds that cache from aGetCallerIdentitythe caller ALREADY issued with the client's own credentials (the default-state-bucket name resolution); it is keyed by the client's credentials, not by a caller-supplied key, so it is a memoization rather than an override and cannot attach an account to credentials the caller does not hold), state-bucket client rebuilder (bucket-region-client.ts—rebuildClientForBucketRegion(client, bucket, opts), the single shared helper extracted in issue #827 from the three near-identicalensureClientForBucket()copies inS3StateBackend/LockManager/ExportIndexStore; does the cached-region probe + same-region short-circuit (returnsnull= keep the original client) + credential-reusing rebuild that does NOT destroy a shared client by default, with per-store knobsdestroyOldClient/reuseClientCredentials/profile/credentials/tolerateNonStandardClient; kept in its OWN module — not folded intoaws-region-resolver.ts— so the per-store tests'vi.mock('aws-region-resolver.js')ofresolveBucketRegionis still intercepted cross-module, and each store retains its ownclientResolved/resolveInFlightmemoization), stack output buffer (stack-context.ts—AsyncLocalStorage-backed per-stack log buffer used bycdkd deploywhen more than one stack is running concurrently; the logger pushes into the active buffer instead of writing to stdout, and the deploy CLI flushes each buffer atomically when its stack finishes so per-stack output blocks don't interleave), single-flight cleanup memoizer (single-flight.ts— wraps an async cleanup function so concurrent / repeated callers await the SAME underlying invocation; used bycdkd local invoke/local start-apito close the SIGINT-during-outer-finally race against shared mutable state likecontainerId/servers[]/ tmpdir sets), docker subprocess helper (docker-cmd.ts—getDockerCmd()resolves the CLI binary viaCDK_DOCKERenv var for podman / finch / nerdctl parity;runDockerStreaming/spawnStreamingroute every docker subprocess call through streaming spawn so BuildKit's progress output doesn't hit Node'sexecFilemaxBufferceiling, mirror chunks to stdout/stderr when the logger is at debug level (--verbose), and reject with aSpawnErrorcarrying the captured streams) - vite.config.ts - Vite+ configuration for build, test, lint, format, and tasks
SDK Providers are in src/provisioning/providers/. See README for the full list of supported resource types. Registration is centralized in src/provisioning/register-providers.ts.
SDK Providers are preferred over Cloud Control API for performance -- they make direct synchronous API calls with no polling overhead. Cloud Control API is used as a fallback for resource types without an SDK Provider.