Skip to content

Latest commit

 

History

History
102 lines (88 loc) · 146 KB

File metadata and controls

102 lines (88 loc) · 146 KB
description cdkd's directory layout and per-file purpose notes (Core Directories, Important Files, SDK Providers)
paths
src/**/*.ts

Key Files and Directories

Core Directories

  • 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 state subcommand 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. The cdkd 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 optional readCurrentState method — a CC-API fallback covers the majority of resource types out of the box; SDK Providers add their own readCurrentState incrementally. The two orphan commands operate at different granularities (this is the breaking change in PR #92): cdkd orphan <constructPath>... is per-resource (mirrors upstream cdk 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 (use destroy / state destroy to 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's DescribeStackResources (issue #1128 / #1130; there is deliberately no aws:cdk:path tag 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 as out of scope and left out of state for the next deploy to CREATE. Matches cdk import --resource-mapping / --resource-mapping-inline semantics, including refusing to silently no-op on a typo'd logical ID; --resource-mapping and --resource-mapping-inline are mutually exclusive (matches upstream); (3) hybrid (--auto with 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-mapping in non-interactive CI re-runs (mirrors cdk 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. --force is 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(...) (in src/cli/commands/retire-cfn-stack.ts) issues a single DescribeStackResources against the named CFn stack and merges the resulting Map<logicalId, physicalId> into the import overrides (user-supplied --resource / --resource-mapping entries take precedence). This side-steps cdkd's tag-based auto-lookup — which can't find resources deployed by upstream cdk deploy (that flow doesn't propagate Metadata.aws:cdk:path as an AWS tag, and AWS reserves the aws: tag prefix so cdkd can't add it on the way through either — confirmed empirically in #1128: TagRole with an aws:-prefixed key returns InvalidInput: Tag keys beginning with aws: are reserved for system use, and a cdk deploy-deployed resource carries tags [] while its template Metadata holds the path. The corollary is that the aws:cdk:path tag 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's DescribeStackResources, via the best-effort, never-fatal tryGetCloudFormationResourceMap. 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 lacking cloudformation:DescribeStackResources is not blocked) — so a bare cdkd import MyStack --migrate-from-cloudformation works for both cdk deploy-managed and cdkd deploy-managed stacks. The flag also forces selectiveMode = false regardless of override count (the CFn-derived overrides shouldn't trigger selective mode, which would mark every other template resource out of scope and orphan them after DeleteStack). (2) Import runs and writes state. (3) After state write, retireCloudFormationStack(...) runs the standard DescribeStacks (verify stable terminal state, capture existing Capabilities) → GetTemplate Original-stage (parse JSON, inject DeletionPolicy: Retain + UpdateReplacePolicy: Retain on 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 concurrent cdkd deploy can'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-cloudformation uses 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 / !Sub are 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 inline TemplateBody ceiling are submitted directly; larger templates are uploaded to the cdkd state bucket under cdkd-migrate-tmp/<stack>/<timestamp>.{json,yaml} and submitted via TemplateURL (the transient object is deleted in a finally immediately after UpdateStack, success or failure). Templates over the 1 MB CloudFormation TemplateURL ceiling 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 dedicated cdkd migrate --from-cfn-stack <name> top-level command, which wraps the same end-to-end flow (upstream cdk migrate codegen + 2-pass (sourceLogicalId, synthLogicalId) mapping + cdkd state + optional --retire-cfn-stack). cdkd import --migrate-from-cloudformation is the right tool when a CDK app already exists and you want to take over an existing cdk deploy-managed CFn stack without re-generating the CDK code.

    provider.import support 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 adding import() support to a provider, update that file. Keep entries one-per-line so parallel PRs don't conflict on rebase.

    cdkd import vs upstream cdk 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 import is therefore not atomic. import.ts collects per-resource outcomes (imported / skipped-not-found / skipped-no-impl / skipped-out-of-scope / failed) and only writes state after a final confirmation (--yes to skip). A partial import can be backed out with cdkd 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 (in auto / hybrid modes) or treats them as out 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 an auto-mode resolution and replay it as --resource-mapping in CI.
    • --force semantics 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.
    • auto and hybrid modes are cdkd-specific (whole-stack adoption via physical-name + CloudFormation DescribeStackResources resolution; no aws:cdk:path tag 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-import DescribeStackResources to recover physical IDs (so cdk-deployed stacks work without --resource) → import → state write → post-import UpdateStack (inject Retain; uploaded to the cdkd state bucket via TemplateURL when over the 51,200-byte inline limit, hard-rejected over the 1 MB TemplateURL ceiling) → DeleteStack. No upstream equivalent — cdk import only 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): bare cdkd import (auto / selective / hybrid mode) reports each nested-stack row as unsupported (NestedStackProvider has no import()). cdkd import --migrate-from-cloudformation IS supported recursively (issue #464): it walks DescribeStackResources recursively, writes one v6-keyed state file per nested child (cdkd/<parent>~<childLogicalId>/<region>/state.json) with parentStack / parentLogicalId / parentRegion populated, recursively injects DeletionPolicy: Retain on every leaf resource (parent + every child template), and retires the whole tree via a single parent-side DeleteStack cascade. 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 what NestedStackProvider.create writes 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 of cdkd import in 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::* AND AWS::CloudFormation::CustomResource — the type CDK emits for new cdk.CustomResource(...) without resourceType; both are Lambda-backed Custom Resources that CFn cannot adopt) or has no entry in cdkd state. AWS::CloudFormation::Stack rows are fully supported as of issue #464 PR B2: buildImportPlan routes each row into a dedicated nestedStackRows: NestedStackRow[] list, the orchestrator invokes buildCdkdStateStackTree(rootStackName, region, stateBackend) to recursively load every child state file from cdkd/<parent>~<childLogicalId>/<region>/state.json (failing fast on a torn tree), and runPerStackImportLoop submits 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: Retain plus ResourceIdentifier: { StackId: <child arn> } plus a TemplateURL rewritten to point at the child's AWS-canonicalized template fetched via GetTemplate(Processed) post-IMPORT). Each child cdkd stack <parent>~<childLogicalId> becomes its own CFn stack named <parent>-<childLogicalId> by default via cdkd2cfnStackName (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's AWS::CloudFormation::Stack.Properties.Parameters block by extractChildImportParameters (literal string / number / boolean classification) and then intrinsic-resolved by resolveChildImportParameters — a root-first pre-pass (buildResolvedParametersPerStack, walking flattenCdkdStateTreeRootFirst) runs the deploy engine's IntrinsicFunctionResolver so {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 a logger.warn + the child template's Default (the pre-resolver behavior), so adding resolution never regresses a working export. The original "one atomic --include-nested-stacks IMPORT changeset" design was found infeasible by the 2026-05-24 AWS spike (AWS rejects that flag combination with ValidationError: 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 via cloudformation:DescribeType (with a hardcoded fallback table in src/cli/commands/export.ts for ~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; inject DeletionPolicy: Delete on resources missing the attribute — matches the CFn type-default for resources without explicit RemovalPolicy; overlay each resource's ResourceIdentifier onto its Properties so CFn IMPORT's identifier-match check passes against cdkd's stack-name-prefixed physical ids), then issues CreateChangeSet --change-set-type IMPORT → wait → ExecuteChangeSetwaitUntilStackImportComplete, and finally deletes cdkd state for the migrated stack. AWS resources are unchanged across the migration; the stack is then managed by cdk deploy / aws cloudformation. Context preservation guard: refuses by default if CLI -c key=value overrides are supplied, because those values are not persisted to cdk.json / cdk.context.json and a subsequent cdk deploy without the same -c flags would synthesize a different template (drift / replacement on first post-migration deploy). User moves the values to cdk.json's context: {} field (recommended) or passes --accept-transient-context to opt in to the risk. On success, prints the exact cdk diff / cdk deploy command including any captured -c flags. 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), inline TemplateBody only (51,200-byte cap), synth template used verbatim (no observedProperties substitution). Caveats: (1) replacement risk on next cdk deploy if the CDK code does not specify explicit physical names (bucketName: 'my-bucket-12345') — same long-standing UX as upstream cdk import; users should set explicit names before exporting or inspect the post-import changeset before executing. (2) cross-stack Fn::GetStackOutput consumers 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 in src/cli/commands/export.ts.

    state is a parent command for inspecting and manipulating cdkd's S3 state bucket: state info prints bucket name, region (auto-detected via GetBucketLocation), the source that resolved the bucket (cli-flag / env / cdk.json / default / default-legacy), the schema version, and a stack count (with --json for tooling); state list (alias ls) 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 v6 parentStack / parentRegion fields and renders a tree(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 --json emits the nested JSON shape for tooling; orphan children whose parent record is missing surface at root level rather than vanishing); state resources <stack> and state 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) reuses buildCdkdStateStackTree (from src/cli/commands/export.ts) to recursively walk every AWS::CloudFormation::Stack row in the target's state and append each child's full state block after the parent's (DFS order, flat at column 0 with Nested stack: <name> headers; --show-nested --json emits the recursive {state, lock, children: [...]} shape with children always 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 new cdk orphan); cdkd orphan <constructPath>... is the synth-driven, per-resource counterpart (mirrors upstream cdk orphan --unstable=orphan) — it removes specific resources from a stack's state file by construct path (MyStack/MyTable), live-fetching every Fn::GetAtt it has to substitute via the resource's provider.getAttribute() (cached per (orphan, attr)) and rewriting every sibling Ref / Fn::GetAtt / Fn::Sub / dependencies reference 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 whose aws:cdk:path is exactly the input OR starts with <input>/, so an L2 path like MyStack/MyConstruct/MyBucket resolves to the synthesized L1 child MyStack/MyConstruct/MyBucket/Resource, and an L2 wrapper that contains multiple CFn resources orphans every child under it. The aws:cdk:path index in src/cli/cdk-path.ts excludes AWS::CDK::Metadata resources so the synthesized <Stack>/CDKMetadata/Default sentinel is never offered as an "available path" and cannot be orphaned; unresolvable references hard-fail with a one-shot list of every site, and --force falls back to the orphan's state.attributes cache (logging a per-case warning) before leaving the original intrinsic untouched if the cache also lacks the attr; --dry-run prints the rewrite audit table without acquiring a lock or saving state. The implementation lives in src/analyzer/orphan-rewriter.ts (the recursion structure mirrors IntrinsicFunctionResolver but in the inverse direction: only orphan references are substituted, every other intrinsic is left alone) and src/cli/cdk-path.ts (the shared aws:cdk:path index, also used by cdkd import). The pre-PR cdkd orphan <stack> whole-stack behavior is gone — the command hard-fails with a redirect message that points to cdkd 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 to cdkd destroy). The per-stack destroy logic is hoisted into src/cli/commands/destroy-runner.ts and shared by both cdkd destroy and cdkd state destroy. As of #555 A2, state destroy is ALSO the documented escape hatch for directly destroying a nested-stack child — cdkd destroy <child> is refused with NestedStackChildDirectDestroyError (matches CFn's "you can't directly destroy a nested stack" semantic; the parent's AWS::CloudFormation::Stack row would otherwise point at gone-from-AWS resources and the parent's next deploy would try to recreate them), but cdkd state destroy <child> intentionally bypasses the guard for users who accept leaving the parent's reference dangling. state migrate copies 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 --verbose to surface it in debug logs, or use state info for an explicit on-demand answer.

  • 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, and cdkd local start-agentcore building blocks (renamed from src/local-invoke/ to share the directory with the rest of the cdkd local family — see PR #228). The start-cloudfront + start-agentcore commands are THIN factory pass-throughs whose command files live at src/cli/commands/local-start-cloudfront.ts / local-start-agentcore.ts — each wraps a cdk-local createLocalStart*Command factory, re-hands the active embed config, AND threads cdkd's --from-state factory through the factory's extraStateProviders seam (issue #766; the start-agentcore factory carried the seam from the start, the start-cloudfront factory gained it in cdk-local 0.128.0 / cdk-local#426). Both layer the cdkd-specific --from-state / --state-bucket / --state-prefix flags on top of cdk-local's inherited --from-cfn-stack / --stack-region (cdk-local#380 also gave start-cloudfront Lambda Function URL + deployed-S3 origins). The ECS run-task family adds ecs-task-resolver.ts (synth template → ResolvedEcsTask with 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), and ecs-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 added ecs-service-resolver.ts + ecs-service-runner.ts + Cloud Map cloud-map-registry.ts / cloud-map-resolver.ts modules 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 bundled runEcsServiceEmulator engine + deleted them from cdkd's tree. The per-CLI-run shared docker network (cdkd-local-svc-<rand>, subnet 169.254.171.0/24, sidecar at 169.254.171.2) and the Cloud Map peer-discovery overlay are now engine-owned. See docs/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 invoke modules: lambda-resolver.ts (target → discriminated ResolvedLambda (kind: 'zip' | 'image') carrying StackInfo / logicalId / runtime+handler+codePath for ZIP or imageUri+imageConfig for IMAGE; both variants carry architecture (issue #768) so the ZIP container run pins --platform the same way the IMAGE path always has; reuses cdk-path.ts and stack-matcher.ts), env-resolver.ts (template literals + SAM-shape --env-vars overrides; intrinsic-valued entries warn-and-drop unless --from-state substituted them upstream), state-resolver.ts (PR 2 — pure-functional substituter that walks intrinsic-valued env-var values against state.resources from cdkd's S3 state file; supports Ref / Fn::GetAtt / Fn::Sub, reports per-key unresolved reasons), runtime-image.ts (Runtimepublic.ecr.aws/lambda/<lang>:<v> + source-file extension; v1 supports nodejs18.x / nodejs20.x / nodejs22.x / python3.11 / python3.12 / python3.13), docker-runner.ts (thin execFile/spawn wrappers around docker pull / docker run -d --rm --name <optional> / docker logs -f / docker rm -f + free-port allocator; PR 5 extended runDetached with --platform / --entrypoint / --workdir; PR 8a added the optional --name for orphan-sweep), docker-image-builder.ts (PR 5 — local-build path for container Lambdas, wraps the shared src/assets/docker-build.ts helper 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), and rie-client.ts (HTTP POST /2015-03-31/functions/function/invocations to RIE inside the container, plus a TCP-probe-based readiness wait). cdkd local start-api modules (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 8b applyAuthorizerOverlay), 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), and http-server.ts (the node:http accept loop with PR 8b authorizer pass and PR 8c's atomic setServerState swap for hot reload). PR 8b additions: authorizer-resolver.ts (REST v1 / HTTP v2 / Function URL authorizer detection + identity-source parsing — extended in #447 with the IamAuthorizer discriminated union member for REST v1 AuthorizationType: 'AWS_IAM', and again in #621 wiring Function URL AuthType: 'AWS_IAM' through the same descriptor so it rides the same SigV4 verifier; #470 added support for Fn::GetAtt: [<UserPool>, 'Arn'] under ProviderARNs[] — the canonical CDK apigateway.CognitoUserPoolsAuthorizer shape — by synthesizing an unreachable placeholder ARN so cognito-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 via STSClient's default credential chain; signature verification only, no IAM policy emulation; warn-and-pass on foreign-identity requests per feedback_match_aws_default_over_opinionated.md). PR 8c additions: cors-handler.ts (CFn CorsConfiguration parser + OPTIONS preflight matcher for HTTP API v2), stage-resolver.ts (per-API Stage selection + attachStageContext for routes; populates event.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-owned cdkd local invoke-agentcore --watch reload loop — built on cdk-local's exported watch primitives (createFileWatcher / createWatchPredicates / resolveWatchConfig / classifySourceChange, the same ones local start-api --watch uses) plus copies of cdk-local's not-exported loadAgentCoreAssetContext / deriveOldAssetHash helpers; it takes rebuild / softReload callbacks from the command so the per-firing classifier picks a docker cp+restart soft-reload (interpreted-handler source edit) vs a full image rebuild (Dockerfile / compiled / asset-hash change), re-opening the /ws socket or re-running the one-shot /invocations on each reload. cdk-local's own runAgentCoreWatchLoop could not be shimmed because it hard-couples to cdk-local's Synthesizer / LocalInvokeAgentCoreOptions types. 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 + ECS ContainerImage.fromEcrRepository) — tryResolveImageFnJoin + substituteImagePlaceholders + the ImageResolutionContext / FnJoinResolveOutcome types, used by both lambda-resolver.ts and ecs-task-resolver.ts. intrinsic-lambda-arn.ts (issue #286 Gaps 3 / 4) is the sibling helper for Lambda ARN intrinsics in API Gateway resolvers — resolveLambdaArnIntrinsic accepts Ref / Fn::GetAtt: [..., 'Arn'] / the REST v1 invoke-ARN Fn::Join wrapper (also emitted by CDK 2.x's HTTP v2 HttpLambdaAuthorizer) / the Fn::Sub invoke-ARN wrapper (both 1-arg ${LogicalId.Arn} form and 2-arg Fn.sub(template, vars) form). Returns a discriminated union so each call site (route-discovery.ts for IntegrationUri, authorizer-resolver.ts for AuthorizerUri) wraps the unsupported case with its own error class. intrinsic-utils.ts (#471) holds the shared pickRefLogicalId helper — extracts the referenced logical ID from a {Ref: <string>} intrinsic, returns null otherwise. Consumed by route-discovery.ts, websocket-route-discovery.ts, authorizer-resolver.ts, and stage-resolver.ts. Centralizes a 5-line predicate that was previously duplicated four times so future intrinsic-shape extensions (e.g. accepting Fn::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 by http-server.ts's buildAuthorizerContextForServiceIntegration (HTTP API v2 service-integration $context.authorizer.* parameter-mapping context). Owns the bare per-kind shape (Lambda flat principalId + context, IAM principalId only, Cognito {claims}, JWT {jwt: {claims, scopes}}). The sibling buildOverlay in http-server.ts (Lambda AWS_PROXY event overlay) still uses hand-rolled per-kind branching because it wraps the result in the AuthorizerEventOverlay discriminated union shape (with the lambda-http-v2 arm layering an additional .lambda namespace); 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.ts is a hand-rolled minimal AWS API Gateway VTL evaluator ($input / $context / $util built-ins, #set / #if / #elseif / #else / #foreach / ## directives, JSONPath subset — no external dep) used by every REST v1 non-AWS_PROXY dispatcher; integration-response-selector.ts resolves IntegrationResponses[].SelectionPattern (regex anchored ^...$) + ResponseParameters header literals + ResponseTemplates Accept-header content negotiation; rest-v1-integrations.ts carries the four dispatchers (dispatchMockIntegration / dispatchHttpProxyIntegration / dispatchHttpIntegration / dispatchAwsLambdaIntegration) plus substituteUriPlaceholders + applyRequestParameters. The CLI commands live at src/cli/commands/local-invoke.ts (creates the cdkd local parent + registers invoke, start-api, run-task, and start-service), src/cli/commands/local-start-api.ts, src/cli/commands/local-run-task.ts, and src/cli/commands/local-start-service.ts. src/cli/commands/local-state-loader.ts is a shared helper (extracted from local-invoke.ts in PR #267) that both cdkd local invoke --from-state and cdkd local run-task --from-state route through to load cdkd's S3 state for the target stack — single impl, parameterized log prefix. It also exports loadBootstrapContainerRepo (issue #1025): a best-effort, never-failing read of the region's asset-storage bootstrap marker (cdkd-bootstrap/{region}.json) that cdkd local run-task --from-state uses to recognize images published to a custom-named cdkd container-asset repo (cdkd bootstrap --container-repo, issue #1011) as cdk-asset images, keeping the local cdk.out-build fast path. Issue #606 layers a LocalStateProvider interface (src/local/local-state-provider.ts) on top, with two implementations: s3-local-state-provider.ts wraps local-state-loader.ts verbatim (the --from-state path) and cfn-local-state-provider.ts reads a deployed CloudFormation stack via cloudformation:DescribeStackResources / DescribeStacks --Outputs / ListExports (paginated, memoized per substitution pass) for the new --from-cfn-stack [<cfn-stack-name>] flag — lets users run cdkd local invoke / start-api / run-task / start-service against CDK apps deployed via the upstream CDK CLI (cdk deploy → CloudFormation) without first migrating to cdkd. The dispatcher lives at src/cli/commands/local-state-source.ts (createLocalStateProvider(options, cdkdStackName, synthRegion) returns the right provider for the supplied flags, enforces mutual exclusion between --from-state and --from-cfn-stack, and resolves the bare-form --from-cfn-stack to the cdkd stack name verbatim). The dispatcher is a thin shim around the cdk-local npm package: cdk-local owns the --from-cfn-stack implementation + the dispatch logic, and cdkd injects its S3-backed --from-state factory via cdk-local's extraStateProviders hook. cdkd's own cfn-local-state-provider.ts is 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: RefDescribeStackResources lookup; Fn::ImportValueListExports; Fn::GetAtt is warn-and-dropped in v1 for most sites (CFn does not return per-attribute values from DescribeStackResources), but as of cdk-local@0.10.0 a consumer Lambda's OWN env-var Fn::GetAtt values are recovered at runtime from the deployed function's already-resolved config (lambda:GetFunctionConfiguration) — CFn resolved every intrinsic at deploy time, so the function's Environment.Variables already carries the concrete value; cdkd inherits this through the local-state-source shim (cdk-local's CfnLocalStateProvider does the recovery; the optional resolveDeployedFunctionEnv provider method is implemented only on the CFn provider, so cdkd's S3 --from-state provider is unaffected). Other Fn::GetAtt sites (e.g. ECS container env) still warn-and-drop. Fn::GetStackOutput is rejected with a clear pointer (cdkd-specific intrinsic, no CFn equivalent). Region handling reuses --stack-region — no separate --cfn-stack-region flag. Phase 3 shim swap (Batch B): an expanding set of src/local/** modules are now thin re-export shims (export { ... } from 'cdk-local') — the implementations described above are owned by cdk-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 OWN cdkd local command tree (it does NOT use cdk-local's command factories, which install the host embed-config themselves), createLocalCommand() (in src/cli/commands/local-invoke.ts) calls setEmbedConfig(CDKD_EMBED_CONFIG) once at build time so every shim that reads cdk-local's getEmbedConfig() renders cdkd branding (cliName: 'cdkd local' / resourceNamePrefix: 'cdkd-local' / awsBindMountPath: '/cdkd-aws' / envPrefix: 'CDKD') instead of cdk-local's cdkl defaults — cdk-local 0.20.0 (cdk-local#85) exposes setEmbedConfig from 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 for cdk-local@0.15.0 (cdk-local#79) to add type RegistrationHandle to its package entry — the still-local sibling src/local/ecs-service-runner.ts imports that type via ./cloud-map-registry.js, so a bare-shim could not typecheck against 0.14.0 (which exposed only the CloudMapRegistry class). cdk-local@0.14.0 (cdk-local#78) had already ported the cloud-map-registry unit 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 — Lambda Runtime → 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@connections management 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 like resolveRuntimeSpec / UnsupportedRuntimeError / readRequestBody stay reachable via cdk-local's source for the ported tests, not the package entry); runtime-image's only divergence from cdk-local was an embedConfig-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-gateway mapping WebSocket Lambda containers need on Linux native dockerd; cdk-local#483 / issue #784 extended the re-export with resolveHostGatewayExtraHosts / HOST_DOCKER_INTERNAL_GATEWAY — the memoized never-throwing host.docker.internal:host-gateway resolver cdkd's local invoke / run-task adopt so a Lambda / ECS container can reach a host-loopback server (AWS_ENDPOINT_URL_* / tunneled VPC); merged into the runner's --add-host list by ecs-task-runner.ts's mergeHostGatewayAddHostFlags, while start-service / start-alb inherit 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), and layer-arn-materializer.ts (materializeLayerFromArn — downloads + unzips a literal-ARN Lambda Layer to a host tmpdir for /opt bind-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-only parseDockerVersion / compareDockerVersions / routeMatchesIdentifier / LayerMaterializationError stay reachable via cdk-local's source for the ported tests, not the package entry); docker-version + api-server-grouping were byte-identical, and layer-arn-materializer's only divergence was the embedConfig-branded tmpdir prefix (getEmbedConfig().resourceNamePrefix renders cdkd's cdkd-local via the host's setEmbedConfig, so behavior is identical). cdkd's consumer tests local-invoke-layers.test.ts / local-start-api-layers.test.ts keep their vi.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 — CFn CorsConfiguration / CloudFront-chain parsing + HTTP API v2 OPTIONS preflight) and intrinsic-image.ts (derivePseudoParametersFromRegion / tryResolveImageFnJoin / substituteImagePlaceholders + ImageResolutionContext — canonical CDK 2.x Fn::Join ECR image-URI resolver + same-stack ECR Fn::GetAtt synthesis). 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-existing isFunctionUrlOacFronted coverage — disjoint helper names, no collision; intrinsic-image's test added under its own filename alongside cdk-local's intrinsic-image-ecr-getatt.test.ts). Both are clean SUPERSET inheritances: cdk-local's cors-handler adds an isFunctionUrlOacFronted export cdkd does NOT consume (a dead export — zero behavior change, lands unwired until the #63 --strict-sigv4 work adopts it); cdk-local's intrinsic-image adds a same-stack-ECR Fn::GetAtt Arn / RepositoryUri synthesis that fires only for a bare-Fn::GetAtt ECR image URI under --from-cfn-stack where the canonical Fn::Join path (unchanged, already resolved pre-shim) did not — docs/local-emulation.md's run-task / start-service --from-cfn-stack warn-drop rows were narrowed to note that exception. The shim re-exports only src-consumed symbols (isFunctionUrlOacFronted / PreflightResponse / FnJoinResolveOutcome stay off the package entry). No breakers. Slice 9 / state-resolver (cdk-local@0.24.0): state-resolver.ts (substituteAgainstState / substituteAgainstStateAsync / substituteEnvVarsFromState / substituteEnvVarsFromStateAsync + the CrossStackResolver / SubstitutionContext / StateEnvSubstitutionAudit / PseudoParameters types — the --from-state / --from-cfn-stack pure-functional intrinsic substituter for env-var / image / role / volume values; Ref / Fn::GetAtt / Fn::Sub / Fn::Join / Fn::Select / Fn::Split plus async Fn::ImportValue / Fn::GetStackOutput via 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 added applyDeployedEnvFallback stay off the package entry — no cdkd consumer). A clean SUPERSET inheritance: cdk-local genericized the per-key unresolved-reason wording (no record in cdkd stateno record in the state source, via cdkd deployand ensure the producer stack was deployed, cdkd-managed stackdeployed stack, need --from-state contextneed 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-stack too); 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 / consumer vi.mock of 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) exposes bufferToBody from its package entry + carries the ported module-own unit test; cdkd drops its bufferToBody (B3 regression guard) block from websocket-server.test.ts. UNLIKE every prior slice this is NOT a bare export { 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-local websocket-server.ts imports bufferToBody as a namespace (import * as websocketBody) and the B4 regression test (Issue #537 item 6) installs vi.spyOn(websocketBody, 'bufferToBody') to assert the post-$connect-deny close-handshake window does no bufferToBody allocation work — a bare re-export binding is a non-configurable getter vi.spyOn cannot 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 the local-start-api-websocket Docker integ. Slice 11 / cluster #4 (cdk-local@0.30.0): cloud-map-resolver.ts (buildCloudMapIndex + CloudMapIndexstart-service Cloud Map service-discovery index from AWS::ServiceDiscovery::PrivateDnsNamespace / ::Service) and integration-response-selector.ts (selectIntegrationResponse / evaluateResponseParameters / pickResponseTemplate / tryParseStatus + IntegrationResponseEntry — REST v1 IntegrationResponses[] selection by SelectionPattern regex / ResponseParameters header literals / Accept content 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-resolver throws EcsTaskResolutionError (owned by still-local ecs-task-resolver.ts) and integration-response-selector throws VtlEvaluationError (owned by still-local vtl-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.ts toThrow(EcsTaskResolutionError); rest-v1-integrations.ts's instanceof VtlEvaluationError catch + vtl-engine.test.ts / rest-v1-integrations-issue-507.test.ts assertions) reference cdkd's LOCAL class — two distinct class objects across the package boundary, so instanceof / toThrow would silently fail. Resolved by the class-identity reconciliation: cdk-local#109 ALSO exports EcsTaskResolutionError + VtlEvaluationError from its package entry, and cdkd's still-local ecs-task-resolver.ts / vtl-engine.ts now DELETE their local class definitions and import { ... } 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 / SelectedIntegrationResponse stay off the package entry; integration-response-selector's old export { 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 tests vi.mock rie-client.js, a dep of rest-v1-integrations itself, not of the shimmed selector). Verified end-to-end via the local-start-service + local-start-api-rest-v1-non-proxy Docker 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), and sigv4-verify.ts (defaultCredentialsLoader + CredentialsLoader) become re-export shims; lambda-authorizer.ts + authorizer-context.ts are DELETED (not shimmed) — once http-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 memory feedback_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-sigv4 flag; cdk-local ships warn-and-pass-by-default with an opt-IN --strict-sigv4 flag. 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-local local-start-api.ts translates its flag to cdk-local's existing sigV4Strict startApiServer option (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 bundled sigv4-verify) are parameterized via two new embedConfig fields — cdkd's CDKD_EMBED_CONFIG sets sigV4StrictByDefault: 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's oacFronted Function-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.ts mocking rie-client.js's invokeRie) is resolved by those test suites moving into cdk-local (where rie-client is in-bundle mockable). cdkd keeps local-start-api.ts local (its --allow-unverified-sigv4 flag + the option translation + the cdkd-glue local-embed-config.test.ts), so the cdkd CLI surface is unchanged. Verified end-to-end via the local-start-api Docker 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_CONFIG flipped to sigV4StrictByDefault: false + sigV4OptFlag: '--strict-sigv4'; LocalStartApiOptions.allowUnverifiedSigv4 → strictSigv4; the two local-start-api.ts translation sites flipped to sigV4Strict: options.strictSigv4 === true; the CLI option renamed --allow-unverified-sigv4 → --strict-sigv4 with the inverted help text + default; shim header comments in http-server.ts / sigv4-verify.ts updated. BREAKING CHANGE for users who relied on cdkd's prior fail-closed default — they must now pass --strict-sigv4 to opt in. Slice 13 / docker-image-builder (cdk-local@0.33.0): docker-image-builder.ts (buildContainerImage + architectureToPlatform + BuildContainerImageOptionsinvoke local 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 its LocalInvokeBuildError extends CdkdError (cdkd's base) while cdk-local's is CdkLocalError-based, so the slice-11 same-base class-identity reconciliation cannot apply. The fix is a thin wrapper — architectureToPlatform + the BuildContainerImageOptions type re-export directly, but buildContainerImage is wrapped to catch cdk-local's thrown LocalInvokeBuildError and re-throw cdkd's CdkdError-based one at the boundary, so a local-invoke build failure still surfaces with cdkd's exit code / formatting. ecr-puller + ecs-task-runner throw / catch their OWN LocalInvokeBuildError (self-contained — they do NOT call docker-image-builder), so they are unaffected. The cdkd docker-build-executable-retag.test.ts's docker-image-builder re-tag block moved to cdk-local (cdk-local#115); the file's docker-asset-publisher block (cdkd ECR publish path, stay-local) stays. Verified end-to-end via the local-invoke-container Docker integ. Slice 14 / file-watcher (cdk-local@0.34.0): file-watcher.ts becomes a bare re-export shim (createFileWatcher + FileWatcher / FileWatcherOptions types). UNLIKE every prior shim this was a user-approved BEHAVIOR-CHANGING feature reconciliation, not a mechanical re-export: cdkd local start-api --watch flips from cdkd's watch-OUTPUT model (watch cdk.out/ + asset dirs; reload only when something else re-synths) to cdk-local's watch-SOURCE model (watch the CDK app source tree at process.cwd(), exclude cdk.out / node_modules / .git, honor cdk.json watch.include / watch.exclude, and RE-SYNTH on a source edit — the cdk watch-like UX). The change was small because cdkd's reloadAllServers ALREADY re-synths (synthesizeAndBuild), so it was a watch-TARGET swap, not a re-synth retrofit. cdk-local@0.34.0 (cdk-local#116) exposes createFileWatcher / FileWatcher / FileWatcherOptions + createWatchPredicates / WatchPredicates + resolveWatchConfig / CdkWatchConfig. cdkd's still-local local-start-api.ts imports createWatchPredicates + resolveWatchConfig from cdk-local, watches [process.cwd()] with cdk-local's ignored / shouldTrigger predicates, and DELETES the watch-output plumbing (computeAssetPaths, lastAssetPaths, the FileWatcher.update() dynamic-path calls, and the corresponding reloadAllServers args). cdkd's file-watcher.test.ts drops (cdk-local owns it). No self-fire loop: cdkd's synth writes only to cdk.out, which createWatchPredicates excludes. Verified end-to-end via the local-start-api Docker integ. The Phase 3 shim swap is COMPLETE — every shimmable src/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 (the ecs-* 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-provider plumbing) plus the CLI command files that keep cdkd's own command tree. NOTE route-discovery.ts's error strings still emit a go-to-k/cdkd docs URL via cdk-local until the cdk-local self-containment cleanup parameterizes it via embedConfig; 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.ts re-exports resolveAlbFrontDoor / isApplicationLoadBalancer + the front-door type set from cdk-local, src/cli/commands/ecs-service-emulator.ts re-exports runEcsServiceEmulator / addCommonEcsServiceOptions + the engine's EcsServiceEmulatorOptions / EmulatorStrategy / Planned* types from cdk-local/internal, and the command file src/cli/commands/local-start-alb.ts (createLocalStartAlbCommand) wires its LocalStartAlbOptions (cdkd-specific --from-state / --state-bucket / --state-prefix + tls?: boolean extending the engine's EcsServiceEmulatorOptions) into runEcsServiceEmulator(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's addAlbSpecificOptions(cmd) (added in cdk-local 0.64.0 / cdk-local#203) so cdkd auto-inherits any new ALB-only flag the upstream cdkl start-alb adds without manual .addOption(...) duplication; parseLbPortOverrides / resolveAlbTarget / albStrategy live in cdk-local and are re-exported by cdkd's ecs-service-emulator.ts shim. 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 (with X-Forwarded-Proto: https preserved); 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-arg extraStateProviders is sourced from the new export cdkdExtraStateProviders in src/cli/commands/local-state-source.ts ({ fromState: fromStateFactory }) — the same factory createLocalStateProvider registers for the rest of the cdkd local * family — so cdk-local's engine picks cdkd's S3-backed --from-state factory transparently when it calls createLocalStateProvider internally 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.ts collapses from a 944-line per-replica orchestrator to a ~120-line shim mirroring local-start-alb.ts's shape (LocalStartServiceOptions extends EcsServiceEmulatorOptions with cdkd's --from-state / --state-bucket / --state-prefix, a serviceStrategy(options): EmulatorStrategy returns boots only with empty lbPortOverrides and no frontDoor, and createLocalStartServiceCommand wires runEcsServiceEmulator(targets, options, serviceStrategy(options), cdkdExtraStateProviders)). The start-service-specific flags (--host-port since cdk-local 0.62.0; --watch since cdk-local 0.69.0 / cdk-local#214 Phase 4) are registered via cdk-local's addStartServiceSpecificOptions(cmd) so cdkd auto-inherits any new start-service-only flag the upstream cdkl start-service adds without manual .addOption(...) duplication. --watch on either start-service or start-alb runs 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, no docker 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 complete vs verdict=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 is tests/integration/local-start-service-watch-fast/. The retained src/local/ecs-network.ts exports — createTaskNetwork / destroyTaskNetwork / buildMetadataEnv / buildEndpointSubnet / METADATA_ENDPOINT_IMAGE / METADATA_ENDPOINT_IP — are kept ONLY because ecs-task-runner.ts (the still-local cdkd local run-task orchestrator) consumes them; once run-task migrates to a cdk-local engine of its own those exports become deletable too.

Important Files

  • src/cli/config-loader.ts - Config resolution (cdk.json, env vars for --app and --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 full cdkd Commander tree (every create*Command() factory, .name / .description / .version). Split out of index.ts for the same reason pipe-close-handler.ts was: importing index.ts runs main() as a side effect, so tooling could not read the command tree without executing the CLI. index.ts's main() now calls it. The consumer that motivated the split is scripts/check-integ-cli-flags.ts (via tests/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 --help omits hidden options and src/cli/options.ts is a flat global list carrying no command attachment (the cdkd import --region bug, issue #1097).
  • src/cli/pipe-close-handler.ts - installPipeCloseHandler() — attaches an 'error' listener to process.stdout / process.stderr so 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 of main() in src/cli/index.ts. Kept in its own module (not inline in index.ts) so it stays unit-testable — importing index.ts runs main() as a side effect.
  • src/cli/commands/diff-recursive.ts - Recursive nested-stack diff helpers backing cdkd diff --recursive (issue #555 A5). Owns buildDiffTree (walks each AWS::CloudFormation::Stack row → child synth template + child state at cdkd/<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 template Parameters defaults via resolveParameters with the nested-stack input parameters as user values, evaluates Conditions, prunes condition-false resources via filterResourcesByCondition, and threads parameters + conditions into the resolver context so Ref/Fn::Sub/Fn::FindInMap/Fn::If resolve like they do on deploy — issue #1027; binding failures fall back to the raw-template diff), readNestedTemplate / indexNestedChildTemplates (template-file loaders mirroring NestedStackProvider's private copies — duplicated to keep the CLI layer off the provisioning layer), nodeHasChanges / treeHasChanges (real-change detectors powering --fail), diffTreeToJson (nested --json shape, NO_CHANGE dropped, children always present), and renderChangeLines / renderDiffTree (the human text renderer, moved out of diff.ts so it is unit-testable without the synth/AWS-client pipeline; a property side whose WHOLE value is an unresolved intrinsic — a Ref/Fn::GetAtt to a resource the same deploy will CREATE — renders as the compact raw intrinsic annotated (known after deploy) instead of collapsing to undefined, and the diff's best-effort resolver contexts set ResolverContext.bestEffort so the resolver's Ref-not-found log is debug there, warn on deploy-time resolution — issue #1017). diff.ts is now thin glue: synth → buildDiffTree per target stack → render / JSON / --fail. NOT in the integ-destroy markgate scope (diff never destroys). cdkd diff --fail exits 1 on any change (CDK parity with cdk diff --fail); plain cdkd diff always 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 DescribeStackEvents equivalent. deployment-events.ts defines the DeploymentEvent / DeploymentEventRecorder types + extractDeploymentEventError (walks the thrown error's .cause chain for AWS error code / request id). deployment-events-store.ts owns DeploymentEventsStore (the buffering JSONL recorder injected into DeployEngineOptions.eventRecorder / DestroyRunnerContext.eventRecorder — best-effort async flush, never blocks the run, warns once on S3 failure) and DeploymentEventsReader (the read side: region discovery via raw key listing so it survives destroy, run listing, single-run JSONL parse). events.ts is 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 from state.json — no state schema bump). Events carry error + metadata only, never resource properties. The per-resource + rollback events are emitted by DeployEngine (provisionResource / the shared src/deployment/rollback-executor.ts) + destroy-runner.ts's delete loop; the run-level RUN_STARTED / RUN_FINISHED + finalize() are owned by deploy.ts / destroy.ts via the shared src/cli/commands/deployment-events-run.ts bracket helpers (startRunRecorder — returns undefined under --dry-run so no recorder / events; recordRunSucceeded / recordRunFailed). Since issue #1183 the standalone cdkd rollback command ALSO opens a recorder (command: 'rollback', an additive DeploymentRunCommand literal) and emits ROLLBACK_* events under its own runId. The reader's index-fallback (when index.json is missing / corrupt) derives each run's result from its own JSONL's last RUN_FINISHED event and reports UNKNOWN (a DeploymentRunSummaryResult value) for a stream with none — never fabricating FAILED. Retention / purge (issue #885): the deployments/ prefix is kept bounded two ways — (1) the writer self-bounds at finalize() via pruneSupersededRunFiles, deleting {runId}.jsonl streams 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, -y to skip the confirm), routed through DeploymentEventsReader.pruneRuns which deletes the matching streams + rewrites (or removes, when empty) index.json. Both batch-delete via the new S3StateBackend.deleteRawObjects(keys) (chunked to the 1,000-key DeleteObjects ceiling, idempotent). runIdTimestampMs parses a run id's compact-ISO prefix back to epoch ms for the --older-than cutoff. (3) cdkd destroy --purge-events (destroy-only flag) deletes a stack's event history right after a CLEAN, non-interrupted destroy via the exported purgeEventsAfterDestroy(reader, stack, region, {purgeEvents, runResult, interrupted}, logger) gating helper in destroy.ts — best-effort warn-on-failure; skipped on a failed/interrupted destroy so those events stay as post-mortem; called AFTER the run's eventRecorder.finalize() so this run's own events are included in the purge. state destroy does not take the flag (cdkd events prune <stack> --all is 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 of cdk rollback / CFn RollbackStack). Loads the rollback-journal.json (written by the deploy engine at failure time), prints a per-segment plan, and replays it newest-first via src/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.json is deleted too. Reuses setupStateBackend / resolveSingleRegion (exported from state.ts) + startRunRecorder (command: 'rollback'). Flags: --force, --orphan <logicalId> (repeatable), --revert-failed (issue #1198 — opt-in replay of the segment's journaled failedOperations BEFORE its completed ops: failed UPDATE force-reverted to previousState with the ATTEMPTED properties as the diff's previous side, failed CREATE deleted only when a state record matches — and then under its DeletionPolicy (issue #1362: Retain orphans, Snapshot snapshots-then-deletes with --skip-final-snapshot as 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-failed for that shape), --skip-final-snapshot (issue #1358 — data-loss opt-out for a rolled-back CREATE under DeletionPolicy: Snapshot, which otherwise snapshots then deletes; the command also builds stack-region-pinned AwsClients for 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 a listRawKeys scan for rollback-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-than age guard (default 30d), ExpectedBucketOwner on every S3 call; --dry-run plan, y/N confirm, chunked DeleteObjects (1,000) / BatchDeleteImage (100). Shares src/cli/commands/state-file-keys.ts (whole-bucket state/lock key listing + stack (region) descriptor, extracted from bootstrap-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). Owns buildStackTree (flat (stackName, region, parentStack, parentRegion) list → parent → child tree, orphan-child promotion + self-link defense), renderStackTreeAscii (tree(1)-style box-drawing ├── / └── / prefixes), and stackTreeToJson (nested shape for --tree --json with explicit null for absent parent fields). Kept separate from state.ts so the tree-construction logic stays unit-testable without mocking S3StateBackend. The S3 read fan-out (one getState per ref) happens in state.ts's renderTreeMode wrapper — the helper itself is sync.
  • src/cli/yaml-cfn.ts - CFn-aware YAML codec used by cdkd export and cdkd 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 the yaml package'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.ts BEFORE returning to the analyzer / provisioner pipeline (Issue #463). Since issue #1150 the pass is selection-aware: SynthesisOptions.deferMacroExpansion skips it inside synthesize(), and the now-public expandMacrosForStacks(stacks, options) is invoked by deploy / diff AFTER stack selection with only the stacks they will consume (a macro-carrying sibling outside the selection never triggers a CFn round-trip); list and destroy defer 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-level Transform: [...] AND nested Fn::Transform: {...} blocks anywhere under Resources / Outputs / Mappings / Conditions / Rules. Skip Metadata keys 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 in REVIEW_IN_PROGRESS, no prior cdkd-macro-expand-* stack needed — Q1 empirically verified 2026-05-23), waits for ChangeSetStatus: CREATE_COMPLETE, fetches GetTemplate --template-stage Processed (returns the post-expansion template; the SDK types the field as string | undefined but the wire shape may be a parsed object — the helper handles both), and cleans up via DeleteChangeSet + DeleteStack in a finally block (both NotFound-tolerant). For templates that declare Parameters without Default, passes synthetic placeholder values (CFn rejects CreateChangeSet otherwise; the values do NOT leak into the Processed-stage template — Ref: <param> survives intact for cdkd's own resolver). Inline TemplateBody for templates <= 51,200 bytes; uploads to the cdkd state bucket and submits TemplateURL for (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. Throws MacroExpansionError (exit code 2) on every failure mode. Intermittent AWS::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.sleep is the test seam) before the error surfaces.
  • src/synthesis/stack-messages.ts - CDK annotation-message handling (issues #1228 / #1230). collectStackMessages(assemblyDir, artifact) gathers aws:cdk:error / aws:cdk:warning / aws:cdk:info entries from the artifact's inline metadata AND its additionalMetadataFile side file (<artifactId>.metadata.json — the layout current aws-cdk-lib uses instead of inlining; unreadable or wrong-shape referenced side file throws, fail-closed) into StackInfo.messages. processStackMessages(stacks, logger, options?) is the CDK-CLI-parity gate: logs every message at its level ([Error|Warning|Info at /path] …), throws SynthesisError('Found errors') when any given stack carries an error annotation; StackMessageOptions.strict (the --strict flag) additionally throws SynthesisError('Found warnings (--strict mode)') on warnings, ignoreErrors (--ignore-errors) displays-but-never-throws, strict wins over ignoreErrors (CDK CLI failAt precedence). Wired into synth (all stacks) and deploy (final selection, before macro expansion / AWS mutations) via the shared annotationMessageOptions in src/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 optional readCurrentState for the AWS-current snapshot, and pipes the result through src/analyzer/drift-calculator.ts. Auto-selects the single stack in state when no positional arg / --all is given (mirrors cdkd 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 / --revert are 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 optional ignorePaths list (sourced from each provider's getDriftUnknownPaths) to skip state property paths the provider deliberately cannot read back from AWS — e.g. Lambda Code: { S3Bucket, S3Key }, which GetFunction only 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 through src/analyzer/drift-normalize.ts so that AWS returning a tag list, a resource-id/ARN array, or a provider-declared unordered plain-string set (options.unorderedPaths, sourced from getDriftUnorderedPaths) in a different order than the deploy-time snapshot does not surface as phantom drift (the deepEqual walk compares arrays positionally).
  • src/analyzer/drift-normalize.ts - Order-normalization helpers for drift-calculator.ts. canonicalizeTagListsDeep sorts any {Key,...}[] tag list by Key; canonicalizeIdArraysDeep sorts 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 sibling cdk-real-drift tool. Plain-string arrays are NOT canonicalized heuristically (a scalar list can be order-significant); instead canonicalizeUnorderedArraysAtPaths(value, paths) sorts a plain-string array ONLY at an explicit per-provider opt-in path list, sourced from the new optional ResourceProvider.getDriftUnorderedPaths(resourceType) and threaded through calculateResourceDrift's options.unorderedPaths by drift.ts (issue #1096 item 1). Both provider-declared path lists share ONE matcher — the exported matchesPathPrefix(path, entries) (exact match, or entry followed by .), which drift-calculator.ts's isIgnoredPath is 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: isIgnoredPath never sees a path crossing an array (the comparator compares arrays wholesale via deepEqual), whereas the unordered walk descends into array elements and gives them the parent's path, so 'Items.Aliases' is meaningful for getDriftUnorderedPaths but 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. FSxFileSystemProvider declares only WindowsConfiguration.Aliases; SelfManagedActiveDirectoryConfiguration.DnsIps is 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 ElastiCache PreferredAvailabilityZones). Declaring the path here rather than sorting inside the provider's readCurrentState reverse-mapper is load-bearing: the normalizer runs on BOTH sides, so it stays correct for the properties-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 DeployEngine so BOTH the in-process automatic rollback AND the standalone cdkd rollback command drive identical semantics. Owns the CompletedOperation / FailedOperation types (the former moved here from deploy-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 carries effectiveProvisionedBy — the record-first route resolution — so the preview can consult the SAME finalSnapshotMechanism matrix 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-failed opt-in path for the op that FAILED mid-deploy; its delete of a provisioned-but-failed CREATE honors the CURRENT state record's DeletionPolicy through the SAME matrix as the completed-CREATE path — orphan-failed-create-retain for Retain, delete-failed-create-with-final-snapshot for Snapshot, plain delete-failed-create otherwise, issue #1362), and sortRollbackCreates. A replacement op (previousState.physicalId !== op.physicalId) is reverted by REVERSING the replacement (issue #1199): re-create the old resource from previousState via its recorded provisionedBy route then delete the new one (create-first; name collision falls back to delete-new-first + bounded name-release retry), or — under UpdateReplacePolicy: 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 record DeletionPolicy governs its delete (CFn semantics) — Retain ORPHANS (dropped from state, left in AWS), Snapshot routes to the delete-with-final-snapshot action which snapshots THEN deletes through the same mechanism matrix as the deploy engine's prepareFinalSnapshotForDelete (atomic delete parameter on the SDK route, createPreDeleteFinalSnapshot for PRE_DELETE_SNAPSHOT_TYPES, refusal-as-per-op-failure for a cc-api-routed atomic type or any other Snapshot-tagged shape) unless RollbackExecutorContext.skipFinalSnapshot (cdkd rollback --skip-final-snapshot) opts into the data loss — issue #1358, which fixed the pre-existing leak where Snapshot orphaned alongside Retain and handed the user an untracked, billing resource; RetainExceptOnCreate and absent / Delete delete plainly — and replay is idempotent (skips already-reverted / physical-id-mismatched / absent resources). Depends only on ProviderRegistry + region + logger + an optional event recorder / per-op state-save hook / finalSnapshotClients (region-pinned PreDeleteSnapshotClients, falling back to getAwsClients()) / skipFinalSnapshot — NOT on DagBuilder / 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 withRetry in src/deployment/retry.ts to 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 exported RETRYABLE_ERROR_MESSAGE_PATTERNS, so retryability has ONE source of truth while isIamPropagationError(message) can select the DENSE retry cadence for the propagation class (see retry.ts). A misfiled pattern only changes the cadence, never whether the error is retryable. Exports isThrottlingError(error) — the single bounded error + .cause walk for rate-limit signals (throttling error NAMES and retryable HTTP statuses, both checked at every depth up to 5). isRetryableTransientError layers the message-pattern table on top of it. Also exports isNameCollisionError(message) (issue #1207) — the "already exists" name-collision matcher shared by the deploy engine's replacement create-first collision detection, its --replace delete-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 adds isNameCooldownError(message) (the SQS QueueDeletedRecently / "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) and isRecreateRetryableError(message) (collision OR cooldown — the retry filter for the delete-then-re-create sites — the --replace delete-first fallback, the --recreate-via-cc-api / --recreate-via-sdk-provider destroy-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 after Invalid 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 explicit maxRetries / initialDelayMs / maxDelayMs / isRetryable means 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 to retryable-errors.ts.
  • src/provisioning/import-helpers.ts - Shared helpers for ResourceProvider.import: resolveExplicitPhysicalId (trust --resource knownPhysicalId, else read the template's physical-name property) and normalizeAwsTagsToCfn (re-shape any AWS tag list — {Key,Value} / {TagKey,TagValue} / map / lowercase — into the canonical CFn Tags shape, stripping aws:-prefixed entries so a CDK-deployed resource's reserved tags never fire false-positive drift). There is deliberately no aws:cdk:path tag walk: AWS rejects aws:-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 former import-tag-walk.ts helper + every provider's tag walk). Auto-mode import resolves any remaining ids from a same-named CloudFormation stack's DescribeStackResources (issue #1128 / #1130), in src/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 adds cdkd bootstrap --asset-bucket / --container-repo custom-name overrides, validated pre-AWS-call, marker-carried, differing-names re-bootstrap hard-errors ASSET_STORAGE_NAME_CONFLICT), the per-region bootstrap marker at s3://{stateBucket}/cdkd-bootstrap/{region}.json (ensureAssetStorage creates the asset bucket + IMMUTABLE-tag ECR repo + marker-last from cdkd bootstrap; --no-assets opts out; owned-elsewhere buckets are hard-refused and every probe passes ExpectedBucketOwner), and the deploy-time AssetModeResolver (marker absent → legacy mode, byte-identical + one cdk gc-hazard info line per legacy region naming the cdkd bootstrap --region <r> fix; present → cdkd-assets mode 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 same ensureAssetStorage on 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; useCdkBootstrapAssets opt pins legacy with no marker read + no notice, suppressLegacyNotice quiets diff / import). cdkd state info lists opted-in regions. The teardown counterpart is src/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-bucket adds 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-shaped cdk-[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::Sub template strings + folded pseudo-parameter-only Fn::Join runs), findUnrewrittenAssetReferences (the deploy engine's §7-step-3 post-resolution audit via DeployEngineOptions.assetRedirect — a surviving CDK-bootstrap reference fails the resource before provisioning), redirectFileAsset / redirectDockerAsset (publish-time redirection consumed by AssetPublisher.addAssetsToGraph({redirect}) — the SAME table as the rewrite so they cannot diverge), createAssetRedirectResolver (lazy STS + marker gate for diff / import), loadPublishableAssetManifest (asset-less stacks stay byte-identical). Rewrite call sites: deploy.ts (top-level), NestedStackProvider.readChildTemplate via NestedStackProviderContext.assetRedirect, diff-recursive.ts's buildDiffTree, import.ts (top-level + recursive CFn-migration child walk); synth / export unrewritten by design (§7.1). --use-cdk-bootstrap-assets (deploy/diff/import/publish-assets) or cdk.json context.cdkd.useCdkBootstrapAssets pins legacy. Integ: tests/integration/asset-migration/.
  • src/assets/docker-build.ts - Shared docker build invocation reused by docker-asset-publisher.ts (ECR publish path), src/local/docker-image-builder.ts (cdkd local invoke container Lambda path), and src/local/ecs-task-runner.ts (ECS run-task ContainerImage.fromAsset path). Streams output via runDockerStreaming (no execFile maxBuffer ceiling — fixes silent kills on # syntax=docker/dockerfile:1 Dockerfiles where BuildKit progress + frontend pull exceeds the prior 50 MB cap). Sets BUILDX_NO_DEFAULT_ATTESTATIONS=1 in the build env (matches CDK CLI's cdk-assets-lib). Full BuildKit flag set forwarded from the CDK DockerImageSource schema (--build-context / --secret / --ssh / --network / --cache-from / --cache-to / --no-cache / --platform). Supports both directory and executable source 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, the ROLLBACK_JOURNAL_VERSION constant, parseRollbackJournal (JSON parse + validation), and UnknownRollbackJournalVersionError. The journal is a sibling of state.json ({prefix}/{stackName}/{region}/rollback-journal.json), deliberately NOT part of the state schema — its own journalVersion (starting at 1), no StackState.version bump. Read/written via S3StateBackend.{load,appendSegment,popSegment,delete}RollbackJournal; deleteState sweeps the key so cdkd destroy cleans 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 constants S3_AUTO_DELETE_OBJECTS_TAG (aws-cdk:auto-delete-objects, stamped by autoDeleteObjects: true) and ECR_AUTO_DELETE_IMAGES_TAG (aws-cdk:auto-delete-images). Consumed by S3BucketProvider.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), and ECRProvider.delete (force: true only with EmptyOnDelete: true, the tag, or forceDataDelete) — 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: Snapshot support (issues #1352 / #1353 / #1354): ATOMIC_FINAL_SNAPSHOT_TYPES (RDS DBInstance / DBCluster, Neptune / DocDB clusters, ElastiCache CacheCluster — the delete call sites generate buildFinalSnapshotIdentifier(physicalId, resourceType) and thread it via DeleteContext.finalSnapshotIdentifier; each provider flips its delete from SkipFinalSnapshot: true to the API's atomic final-snapshot form; ONLY on the SDK route — a cc-api-routed atomic type is refused and CloudControlProvider.delete fail-closes on the field), PRE_DELETE_SNAPSHOT_TYPES + createPreDeleteFinalSnapshot dispatcher (all CC-routed: AWS::EC2::Volume via EC2 CreateSnapshot tagged cdkd:final-snapshot-of; AWS::Redshift::Cluster via CreateClusterSnapshot; AWS::ElastiCache::ReplicationGroup via ElastiCache CreateSnapshot — each waited to ready, idempotent reuse via the tag / the finalSnapshotNamePrefix name prefix across delete re-runs), unsupportedFinalSnapshotError / ccRoutedFinalSnapshotError refusals, and (issue #1366) finalSnapshotMechanism(type, route) / refusesFinalSnapshot(type, route) — the mechanism matrix as a PURE function, so the executor that ACTS on it and the cdkd rollback plan 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 — finalSnapshotMechanism tests the atomic set first, so a type in both would silently take the atomic arm and never reach the pre-delete snapshot; pinned in final-snapshot.test.ts alongside the union-equals-the-CFn-documented-list fence (re-homed there from the deleted supportsFinalSnapshot predicate, #1368). Consumed by the deploy engine (prepareFinalSnapshotForDelete — the shared gate for the DELETE branch AND the four replacement / recreate delete sites), destroy-runner.ts, and rollback-executor.ts — the latter twice: rollbackFinalSnapshotId for the delete-of-the-NEW-resource under UpdateReplacePolicy (honors only the atomic SDK-routed shape, plain-deletes otherwise — scope decision on #1354), and prepareCreateRollbackFinalSnapshot for a rolled-back CREATE under DeletionPolicy (the FULL matrix, refusing what it cannot snapshot — issue #1358). The engine's clients come from DeployEngineOptions.finalSnapshotClients (stack-region-pinned AwsClients, structurally a PreDeleteSnapshotClients), threaded on to RollbackExecutorContext.finalSnapshotClients; --skip-final-snapshot (deploy / destroy / state destroy / rollback, skipFinalSnapshotOption in src/cli/options.ts — deliberately NOT in the shared destroyOptions array cdkd orphan consumes) 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 (renames Configuration.ConfigurationProperties -> the SDK's Properties at EVERY Configurations nesting level), toSdkStepConfigs (HadoopJarStepConfig.StepProperties -> Properties), and toSdkInstanceTypeConfigs (per-instance-type nested Configurations). 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 by EMRClusterProvider (top-level Configurations / Steps, per-group Configurations, per-fleet InstanceTypeConfigs), EMRInstanceGroupConfigProvider (create), and EMRInstanceFleetConfigProvider (create + the ModifyInstanceFleet update). No inverse is needed: Configurations / Steps / InstanceTypeConfigs are all declared in EMRClusterProvider.getDriftUnknownPaths and neither instance provider implements readCurrentState. Non-object / non-array inputs (an unresolved intrinsic) pass through untouched so AWS surfaces the real validation error. The AWS::EMR::* types are NOT yet in NESTED_KEY_TARGETS (scripts/gen-nested-key-coverage.ts) — critic target expansion is tracked in issue #1393.
  • src/provisioning/ec2-termination-protection.ts - Shared --remove-protection helper for AWS::EC2::Instance: disableInstanceApiTermination() (flip DisableApiTermination off, idempotent, errors swallowed at debug), isTerminationProtectionPropagationError() (matches the "may not be terminated. Modify its disableApiTermination" 400 from both TerminateInstances and the Cloud Control DeleteResource wrapper), and TERMINATION_PROTECTION_MAX_ATTEMPTS. Used by EC2Provider.deleteInstance (SDK path) and CloudControlProvider.delete (CC-API path — an instance routes through Cloud Control whenever its template trips the #614 silent-drop routing) so --remove-protection works 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 by ASGProvider.delete (issue #796): an AWS::AutoScaling::AutoScalingGroup whose launch template sets DisableApiTermination: true launches instances that survive the group's DeleteAutoScalingGroup(ForceDelete: true) (ASG-level DeletionProtection + ForceDelete governs only the group + scale-in protection, not EC2-level termination protection), so under --remove-protection the provider enumerates the group's current instances and flips each one's DisableApiTermination off 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 as AvailabilityZoneIds (#614 routing) — Cloud Control's DeleteResource cannot ForceDelete a protected ASG or clear its protection, so CloudControlProvider.delete detects removeProtection === true && resourceType === 'AWS::AutoScaling::AutoScalingGroup' and delegates to new 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 emits availabilityZones names not AvailabilityZoneIds, 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.ts ships the provider-coverage Tier 3 set (ProvisioningType: NON_PROVISIONABLE) into the runtime, codegen'd from docs/_generated/provider-coverage.json by scripts/gen-unsupported-types.ts (vp run gen:unsupported-types; CI fails on drift). The hand-written .ts adds isNonProvisionable() + unsupportedTypeIssueUrl(); both are consulted by CloudControlProvider.isSupportedResourceType (rejects Tier 3) and ProviderRegistry.validateResourceTypes (per-type error + issue link). The --allow-unsupported-types escape hatch routes named types through Cloud Control via ProviderRegistry.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.ts ships per-Tier-1-type { handled, silentDrop } records, codegen'd from tests/fixtures/cfn-schemas/*.json + each SDK provider's handledProperties / unhandledByDesign declarations by scripts/gen-property-coverage.ts (vp run gen:property-coverage; CI fails on drift; the codegen parses provider sources via the TypeScript Compiler API so no dist/ bootstrap is needed). The hand-written .ts adds getPropertyCoverage() + findSilentDropProperties() + unsupportedPropertyIssueUrl(); all are consulted by ProviderRegistry.validateResourceProperties (per-resource per-property error + 1-click GitHub issue link + dedup'd re-run command). The --allow-unsupported-properties escape hatch (deploy only) routes named <Type>:<Prop> entries past the reject via ProviderRegistry.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 the enrichResourceAttributes switch in src/provisioning/cloud-control-provider.ts via the TypeScript Compiler API (per-case enriched['Attr'] keys, flat-keys like Endpoint.Address matched to the nested readOnly prop Endpoint), cross-references each type's readOnlyProperties from the cached CFn schema fixtures (tests/fixtures/cfn-schemas/*.json), and classifies each into enriched / 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 --check critic hard-fails ONLY on unenriched-computed. A readOnly prop that is the type's primaryIdentifier is auto-classified not-a-gap (the resolver's physicalId fallback resolves it); scripts/refresh-cfn-schemas.mjs captures primaryIdentifier into the fixtures for this. The seed ENRICHMENT_ALLOW_LIST carves out AWS::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 of gen-enrichment-coverage.ts (which only audits the CC provider's enrichResourceAttributes switch). Makes the #1179 GetAtt-key bug class (SDK create()/update() records an ARN under a non-CFn key) non-regressing. Output / cross-resource Fn::GetAtt reads the cached resource.attributes[<CFnName>] in IntrinsicFunctionResolver.constructAttribute (which never calls a provider's getAttribute), so an ARN stored under the wrong key is missed and — for a *Arn/*Url name — HARD-FAILS the resolver's shape guard (#1179 stored Arn not AgentRuntimeArn, breaking a CfnOutput). The generator parses (a) each provider's create/update attribute-object keys (collectStoredAttributeKeys, object-literal + element-access-assignment keys — a case '<Attr>': label in getAttribute is deliberately NOT collected, so a provider handling the ARN only in getAttribute is still flagged), (b) the handledProperties maps (which types each provider serves), and (c) the set of types constructAttribute references, all via the TS Compiler API. An Arn/Url read-only attribute (minus primaryIdentifier) is a gap iff it is NEITHER cached by the provider NOR the type is constructAttribute-handled NOR allow-listed — scoped to Arn/Url because those are the ONLY suffixes the resolver's guard hard-fails on (non-ARN attrs warn-and-fallback and are legitimately left uncached). The SDK_ATTR_ALLOW_LIST seeds AWS::SNS::Subscription.Arn (NOT-A-BUG: physicalId IS the subscription ARN, guard fallback resolves it). It initially also carried AWS::Lambda::EventSourceMapping.EventSourceMappingArn as 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 alongside gen-enrichment-coverage.ts (CC attribute enrichment) and gen-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 #1265 LambdaUrlProvider; #1267 -> PR #1268 EventBridge bus / SNS topic / Lambda event-source / Logs log-group). Scans src/provisioning/providers/*.ts PLUS src/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 public update() carrying a protected flag, sets the flag inside any try whose catch raises a ProvisioningError — counting ALL THREE spellings: the literal throw new ProvisioningError(...), the throw-form FACTORY throw this.wrapError(...), and the STATEMENT-form this.handleError(error, ...) with no throw keyword 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 SWALLOWING catch (log-and-continue, where no raw error can propagate; a conditional re-throw, a return Promise.reject(...), or a statement call to a never-returning helper are NOT swallows). A never return alone does not make a method a wrap factory, and a new ProvisioningError buried in a nested closure the method never returns does not either, follows this.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 unresolvable this.x() callee is recorded and surfaces as unresolved-callee rather than silently reporting no-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 in update(), sends in a private applyUpdate() — and the inverse s3-tables shape — bare update(), 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 wrapping catch that can capture a control-flow typed error — raised as throw OR as return 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 positive instanceof test that throws anything else is the #1268 defect itself, and the negated if (!(error instanceof CdkdError)) throw error; is the inverse shape, so both are rejected. CONTROL_FLOW_THROW_CLASSES (blocking) holds ResourceUpdateNotSupportedError — which changes BEHAVIOR when swallowed (the deploy engine matches it by class to fall back to replacement) — plus ProvisioningError, 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 from TYPED_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's handleError), and a THROW-form factory may return the typed error for the caller to throw (lambda-microvm-image's wrapError) — both count; a return inside 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 relabelled wrapped) / unresolved-callee (visible, non-blocking). UPDATE_WRAP_ALLOW_LIST is keyed Class#method via allowKey() 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 EXACT no-aws class set pinned by name (a >= 1 floor would let a class silently drop into no-aws, the shape a lost delegation edge produces, while the aggregate wrapped floor absorbed it), assertions that unresolved-callee AND allow-listed are both zero, a fence that the five #1263/#1267-fixed providers still classify wrapped, 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-coverage compares TOP-LEVEL names only, and the class recurred 4 times before tooling (#1165/#1167 ECS casing, #1160 API GW v2, #1304 MetricTimeZone, #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's nestedProperties capture (added to scripts/refresh-cfn-schemas.mjs by this issue — per-top-level-property nested names, $ref-resolved + cycle-guarded) for the provider's OWN handledProperties top-levels against the SDK client model member names (node_modules/<pkg>/dist-types/models/*.d.ts PropertySignatures 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 (exact for PascalCase SDK models, lower-first for 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-target minNestedKeys + 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: CloudFront OriginCustomHeaders (never renamed to the SDK's CustomHeaders — origin custom headers silently dropped on create AND actively wiped on update by the required-field fill) and ECS TaskDefinition S3FilesVolumeConfiguration (SDK member is the irregular all-lowercase-prefix s3filesVolumeConfiguration, 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 capture definitionShapes (per CFn definition, member -> terminal type kind, $ref-resolved; the top-level block under the reserved #top key), the SDK side parses full interfaces with member type kinds (collectSdkInterfaces + wrapperInterfaceNames — a Quantity-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 CloudFront QUANTITY_ITEM_FIELDS class, so a NEW array member AWS adds flags until wrapped) and definition-member-missing (a CFn definition's member same-spelling an SDK member globally but missing from the same-named SDK interface — the CachedMethods sibling-vs-nested / GeoRestriction.Locations / legacy S3Origin class). Shape evidence uses the DOT-SEGMENT-EXPANDED literal set ('ForwardedValues.Headers' names both segments); the key pass keeps the strict set. Non-blocking visibility: ambiguous shapes + 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 legacy S3Origin the one new allow-list entry (invisible to the key pass because the StreamingDistribution API still carries a same-spelled member). The #1378 rider also gave refresh-cfn-schemas.mjs a --help / unknown-flag guard (an unrecognized flag previously fell through to a silent FULL ~135-type re-fetch). Issue #1430 added AWS::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 found NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled broken in BOTH directions: CFn carries a required boolean while the SDK's block is an EMPTY structure whose PRESENCE enables delivery, so EventBridgeEnabled: false silently ENABLED notifications on the write side and readCurrentState returned 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 positive classifyTargetShapes documents — 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 that same-spelling is 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::Project BuildBatchConfig.BatchReportMode stayed silent even with every occurrence of the SDK spelling batchReportMode renamed away, which is what proved the gap structural). A target setting freshObjectMapper: true requires each would-be-same-spelling key 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), else no-write-evidence. WRITE_EVIDENCE_EXCLUDED_FUNCTION_PREFIXES skips reverse-map bodies by word-boundary PREFIX, because for an exact-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 from s3-bucket-provider.ts, 71 from codebuild-provider.ts, 42 from ecs-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 and BuildBatchConfig.ServiceRole stays 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.convertLinuxParameters is return 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 walk gen-handled-property-wiring already 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 stay provider-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 forward batchReportMode: write from the real codebuild-provider.ts — leaving readCurrentState'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 needs cloudformation:DescribeType).
  • scripts/gen-handled-property-wiring.ts + docs/_generated/handled-property-wiring.{json,md} - handledProperties WIRING 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.ts verifies every CFn property is ACCOUNTED FOR (declared in handledProperties or unhandledByDesign) and gen-nested-key-coverage.ts audits spellings INSIDE a forwarded blob, but neither checks that a handledProperties entry is actually WIRED. ECRProvider declared ImageTagMutabilityExclusionFilters handled 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, and table-loop (properties[k] where k iterates a literal name list — inline array, enclosing-scope const, or Object.entries(TABLE)), plus an orthogonal delegated tag when the read happens in a callable reached by a call edge. Two rules keep table-loop from becoming a rubber stamp, since one syntactic site there credits N properties at once (43 tagged today across 5 classes, 29 of them with table-loop as their ONLY evidence — GlueJobProvider 16, SQSQueueProvider 13). The loop body must DELIVER, not merely compare: EC2Provider.updateSubnet's for (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 some properties[k] in that body escapes comparison (following one const hop, and seeing through JSON.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; RDSDBProxyProvider shows the discrimination cleanly, its immutable-field loop losing the credit while its mutableFields loop (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.ts really does declare result x12, out x5, toAdd x3). 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, a getDriftUnknownPaths entry, the handledProperties declaration itself, and a readCurrentState write-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 via ECRProvider's hasCdkAutoDeleteTag(properties) call in delete(), 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 (isInertWholeBagUse additionally exempts by shape a result that only feeds a comparison or a .length measurement, 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 of previousProperties is 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 single element-read (the desired-side half of properties['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 records seededBy, the class member(s) whose walk produced the evidence, so a property wired only from a non-delivery member such as readCurrentState() is visible rather than silently green (0 today, fenced by a test). HANDLED_WIRING_ALLOW_LIST is 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 as gen-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, with property-read + destructure pinned === 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) and AWS::Logs::LogGroup.ResourcePolicyDocument (issue #1412). BOTH are now fixed and their allow-list entries REMOVED: neither property can be delivered by its SDK provider (no CreateNatGateway member and no NAT gateway modify API for the first; an account-wide AWS::Logs::ResourcePolicy with no CreateLogGroup counterpart for the second), so each moved to unhandledByDesign, which converts the invisible drop into the #614 Cloud Control auto-route. What remains allow-listed is IAMAccessKeyProvider#Serial and NestedStackProvider#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 real ecr-provider.ts to its pre-#1406 state must exit non-zero naming the property (a first probe that stripped only the lowercase-p reads PASSED, because the surviving previousProperties read cleared it; that false clean is what drove the previousProperties exclusion, and both variants are now automated). The real-code probe set also covers the two table rules (stripping properties['VpcId'] from the real ec2-provider.ts must 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 real glue-provider.ts must not borrow buildJobCommonFields's local table), the stale-allow-list verdict (injecting a properties['Serial'] read into the real iam-access-key-provider.ts must report that entry stale — the probe was re-pointed there when the two KNOWN GAP entries retired), and the property-read / destructure recognizers (rewriting the real ECR read into each shape must stay wired). The shipped --check command itself is exercised via spawnSync against a scratch COPY of src/provisioning/providers carrying the injected regression (--providers-dir= test seam), so the exit code and failure text are covered without ever writing to src/. NO AWS integ (pure static analysis / codegen).
  • src/provisioning/describe-type.ts - Shared cloudformation:DescribeType invocation with THROTTLE-ONLY retry (issue #1236), consumed by write-only-properties.ts, create-only-properties.ts, and export.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 (dropped AWS::ECS::Service.VolumeConfigurations -> UpdateService 400, or a registry-only replacement classification). describeTypeWithThrottleRetry wraps the call in withRetry with isRetryable: 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.sleep is the test seam. Also exports hasNoRegistrySchema(resourceType) — the ONE list of types with no CloudFormation registry entry (Custom::*, AWS::CloudFormation::CustomResource, and the AWS::CDK::Metadata synth 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 the AWS::CDK::Metadata resource 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-schema writeOnlyProperties via cloudformation:DescribeType, reduced to top-level containing property names (nested /properties/Foo/Bar strips to Foo), short-circuiting to the empty set for hasNoRegistrySchema types, 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 by CloudControlProvider.update, which strips these properties from the PREVIOUS side before patch generation so the patch always carries add ops 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.VolumeConfigurations hard-fails; other types lose config silently). Mirrors terraform-provider-awscc's prior-state clearing. DescribeType goes through describe-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 or 0 (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 (both Math.max(providerMinTimeoutMs, floor, globalTimeoutMs)). Fixes the opensearch-domain-getatt destroy timeout (a domain delete runs 15-30 min but the flat CC cap was 15 min, so waitForOperation threw DELETE timeout after 900s mid-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-timeout input (issue #1280) — the SDK-provider analogue of slow-cc-operation-timeouts.ts's inner-undercuts-outer fix, sourced from CLI input instead of hardcoded floors. setResolvedResourceTimeouts(opt) is seeded by deploy.ts / destroy.ts / state.ts (state destroy) right after validateResourceTimeouts (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-wait steady-state waiter (cap max(600s, resolved)) and CloudFrontDistributionProvider.waitForDistributionStable's Deployed-wait budget (cap max(20min, resolved), issue #1282 — reached by the --full-wait create/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 (same cloudformation:DescribeType + per-type cache + graceful-degradation pattern). getCreateOnlyPropertyPaths(resourceType) resolves the type's registry-schema createOnlyProperties as FULL segment paths (schema-less types — Custom::* / AWS::CloudFormation::CustomResource / AWS::CDK::Metadata, per the shared hasNoRegistrySchema predicate in describe-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 pure createOnlyChangeRequiresReplacement compares 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 by DiffCalculator.compareProperties as a fallback for any property the hand-authored ReplacementRulesRegistry does not explicitly classify (ReplacementRulesRegistry.isClassified gates it so a deliberate updateableProperties decision 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-recreation stateful guard as --replace, so a template immutable-property change can no longer silently DELETE+CREATE a stateful resource's data. DescribeType goes through describe-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 NO createOnlyProperties at 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-authored ReplacementRulesRegistry entry; when the type is data-bearing, add it to STATEFUL_TYPES in 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.tsgreen / yellow / red / cyan / gray / bold / dim inline wrappers; kept in a separate module from logger.ts so test files that vi.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.tsformatResourceLine(op, logicalId, resourceType, verbOverride?) builds the shared <glyph> <id> (<type>) <verb> line printed by cdkd deploy / cdkd destroy for 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; verbOverride swaps the verb word, e.g. 'updated (metadata)'), live progress renderer (multi-line in-flight task display), error handler (incl. normalizeAwsError for AWS SDK v3 synthetic UnknownError → actionable HTTP-status-keyed messages), AWS client factory, AWS region resolver (aws-region-resolver.ts — caches bucket-region lookups via GetBucketLocation so the state-bucket S3 client can be rebuilt for the bucket's actual region), state-bucket owner guard (expected-bucket-owner.tsresolveExpectedBucketOwner(client) / expectedOwnerParam(client): STS GetCallerIdentity on the client's own credentials, spread as ExpectedBucketOwner into 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-client WeakMap fast 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 shared AwsClients.s3, the region-corrected rebuild) share ONE round trip, while an assumed-role session (its own ASIA… key) still resolves its own account — which is what keeps the cross-account Fn::GetStackOutput RoleArn path correct. recordResolvedAccountId(client, accountId) seeds that cache from a GetCallerIdentity the 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.tsrebuildClientForBucketRegion(client, bucket, opts), the single shared helper extracted in issue #827 from the three near-identical ensureClientForBucket() copies in S3StateBackend / LockManager / ExportIndexStore; does the cached-region probe + same-region short-circuit (returns null = keep the original client) + credential-reusing rebuild that does NOT destroy a shared client by default, with per-store knobs destroyOldClient / reuseClientCredentials / profile / credentials / tolerateNonStandardClient; kept in its OWN module — not folded into aws-region-resolver.ts — so the per-store tests' vi.mock('aws-region-resolver.js') of resolveBucketRegion is still intercepted cross-module, and each store retains its own clientResolved / resolveInFlight memoization), stack output buffer (stack-context.tsAsyncLocalStorage-backed per-stack log buffer used by cdkd deploy when 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 by cdkd local invoke / local start-api to close the SIGINT-during-outer-finally race against shared mutable state like containerId / servers[] / tmpdir sets), docker subprocess helper (docker-cmd.tsgetDockerCmd() resolves the CLI binary via CDK_DOCKER env var for podman / finch / nerdctl parity; runDockerStreaming / spawnStreaming route every docker subprocess call through streaming spawn so BuildKit's progress output doesn't hit Node's execFile maxBuffer ceiling, mirror chunks to stdout/stderr when the logger is at debug level (--verbose), and reject with a SpawnError carrying the captured streams)
  • vite.config.ts - Vite+ configuration for build, test, lint, format, and tasks

SDK Providers

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.