Skip to content

feat: support newer provider plugin-protocol features via targetVersions (RFC-04) - #296

Merged
so0k merged 37 commits into
mainfrom
feat/provider-feature-availability
Jul 23, 2026
Merged

feat: support newer provider plugin-protocol features via targetVersions (RFC-04)#296
so0k merged 37 commits into
mainfrom
feat/provider-feature-availability

Conversation

@so0k

@so0k so0k commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Implements RFC-04 — provider feature availability: support the newer provider plugin-protocol capability families with targetVersions-aware codegen and synth-time validation. Proposal, dataset, sweep tooling and interactive report live in open-constructs/cdktn-planningRFCS/04-provider-feature-availability/ (only the merged matrix is vendored here, matching the function-availability split).

Builds directly on the #269 foundation (targetVersions + ValidateFeatureTargetSupport) and the #268 usage-registry pattern.

Guiding decision

Generate the full surface the schema offers; narrow per project at synth time via targetVersions. Generation-time filtering would fork the generated API by project configuration — impossible for prebuilt providers and hostile to caching. Constraints baked into the validations are the per-product >= ranges from the sweep dataset.

Review guide (one commit per RFC rollout item)

  1. chore: vendor provider-feature availability matrix — dataset digest + README only, no behavior change.
  2. feat(lib): TerraformEphemeralResource — new public base class synthesizing to the top-level ephemeral key (JSON + HCL renderer), refs as ephemeral.<type>.<id>.<attr>, no provisioners/connection/import/move. Constructor registers the target-version validation unconditionally (new API surface — only fires on use, no feature flag per RFC). Internal providerFeatureConstraints map sourced from the vendored matrix.
  3. feat(provider-generator): acquire newer provider-protocol schema sections (Phase 0, a bugfix on its own) — commons types for functions / ephemeral_resource_schemas / resource_identity_schemas / write_only; sanitizer walks ephemeral schemas; fetched schemas stamped with the fetching CLI; cache key now includes CLI product+minor (a schema fetched once with an old CLI no longer poisons every later generation); fetch-time warning when the fetching binary structurally cannot emit sections the targets admit.
  4. feat(provider-generator): generate ephemeral resource bindings — third schema family mirroring the data_ pipeline (EphemeralRandomPassword in ephemeral-random-password/), config extends TerraformEphemeralMetaArguments, no generateConfigForImport. Zero churn in existing snapshots (ephemeral models append after resources/data sources; class-name dedup is order-dependent).
  5. feat(provider-generator): generate provider-defined function bindingsTimeProviderFunctions.rfc3339Parse(ts)${provider::time::rfc3339_parse(...)}; namespace defaults to the registry short name with a providerLocalName override (local names change the namespace, aliases don't). Runtime flows through one public jsii chokepoint (TerraformProviderFunction.invoke) feeding a usage registry; TerraformStack validates usage against terraform >=1.8.0 / opentofu >=1.7.0 — note the deliberate asymmetry: OpenTofu language support (1.7.0) predates its schema emission (1.8.0), so generation and validation use different boundaries.
  6. feat(provider-generator): deprecate write-only attribute getters, validate usage — providers never persist write-only values (every read is null by protocol contract), so the state-backed getter is a trap: emitted @deprecated now, removal rides the next prebuilt major (JSII-breaking otherwise). Setting one (setter or constructor config) registers usage via a new protected TerraformResource.registerProviderFeatureUsage hook — generated code reaches the validation machinery only by extending base classes.
  7. feat(cli): thread targetVersions into cdktn getGetOptions.targetVersionsConstructsMakerreadSchema, driving the Phase 0 fetch-time warning; constraints.json gains diagnostic stamps (targetVersions, fetching cli) without affecting the filterAlreadyGenerated staleness logic (targets don't change codegen output, so they must not force regeneration).

Test coverage

  • Generator: snapshot tests against real terraform providers schema -json fragments from the sweep (random ephemeral, time functions incl. object returns, vault *_wo), plus a synthetic fixture for variadic/reserved-name mapping branches. Zero churn in pre-existing snapshots.
  • Core: validation matrices mirroring validations.test.ts (admit/exclude per product, hint text, the OpenTofu 1.7.0 asymmetry, registry resets).
  • Schema: emission-gap logic matrix, cache-key suffix behavior, sanitizer walk over ephemeral schemas; CLI stamps in network snapshots are stubbed (environment-dependent).

Known/deferred

Review findings addressed (from the demo-harness verification comment)

  • Fixed — targetVersions validation bypass: CdktfConfig.targetVersions (the runGetInDir path) now validates via commons validateTargetVersions (warn + ignore), and the emission-gap check treats invalid ranges as not-wanted instead of throwing — a malformed range previously crashed cdktn get via semver.intersects.
  • Fixed — ephemeral lifecycle narrowed: new TerraformEphemeralResourceLifecycle (precondition/postcondition only) replaces the full managed-resource lifecycle on the ephemeral API, before it ships as jsii surface.
  • Fixed (docs) — provider-fn self-reference cycle: generated provider-function JSDoc and TerraformProviderFunction.invoke now warn against calling a provider's functions inside that same provider's configuration block.
  • Documented — ephemeral × write-only: registration deliberately skips ephemeral resources — write-only is a state concept, ephemeral resources have no state, and no schema in the sweep (incl. vault's 16 ephemeral resources) combines the two.
  • Follow-up — nested write-only registration (Nested write-only attributes don't register targetVersions feature usage #308): write_only inside nested blocks gets the deprecated getter but skips usage registration; deep config scanning deserves its own PR (a miss degrades to the plan-time error, not silent breakage).
  • Pre-existing / by design: the AWS barrel-import OOM predates this PR (lazy-index is the existing mitigation); Fn.ephemeralasnull + sensitive on outputs is Terraform semantics (docs candidate); list resources / actions / resource identity codegen is the RFC Phase 4 deferral noted above.

Second review round addressed

  • Fixed — cross-App usage-registry leak: App construction now resets both usage registries (a new App = a new synthesis session). Within an App the process-global registry is by design — every stack resolves the same targetVersions from App context — but usage no longer leaks into later, unrelated Apps in the same process (the reviewer's jest scenario). Regression tests cover the exact sequence; the flag-gated Fn registry had the same latent leak and is reset too.
  • Fixed — emission-gap warning on cache hits: the warning moved from readProviderSchema into readSchema, running for cached and fresh schemas using the cli_name/cli_version stamps the schema carries. Regression test: two readSchema calls against a cache dir → one producer call, two warnings.
  • Docs — matrix as source of truth: both hand-maintained maps now name features-matrix.json as their source and cross-reference each other; automated drift check tracked in In-repo consistency check for the provider-feature availability matrix and its hand-maintained maps #309.

Third review round addressed (cfncompat/awscc demo findings)

  • Fixed — dynamic-typed provider-function returns coerced to string: generated wrappers for dynamic/object/map return types no longer wrap in Token.asString(...); they return the raw invoke() IResolvable, which Tokenization.isResolvable() recognizes — struct-typed attribute assignments (OutputReference.internalValue) no longer vanish silently from synth output.
  • Fixed — invoke() dropped literal null positional arguments: arguments are now validated per position (variadic flattening stays in the generated wrappers where the signature is known), and a latent third bug this exposed was fixed too: FunctionCall rendered args via Array.prototype.join, which collapses null entries to empty strings — resolved null/undefined now render as the Terraform null keyword. Regression tests pin the exact provider::cfncompat::condition_if(true, null, {"a" = 1}) rendering.

Fourth review round addressed (jsteinich's CHANGES_REQUESTED)

Seven commits on top of the reviewed head, one per finding group (inline replies on each thread carry the details):

  1. Schema fidelityFunctionParameter gains is_nullable, FunctionSignature gains deprecation_message, and both drop description_kind (verified against internal/command/jsonfunction/{function,parameter}.go and the OpenTofu equivalent: that field does not exist on function JSON; allow_null_value/allow_unknown_values are protocol-only names — the old comment was wrong). OpenTofu emits no deprecation_message at all (divergence, documented).
  2. Recursive, honest type mapping — collections recurse (list(number)number[] via Token.asNumberList, set(bool) param → Array<boolean | IResolvable>, nested compose); object/map/dynamic returns are declared IResolvable instead of any (property access on a token is now a compile error); is_nullable consumed (trailing → jsii-optional, mid-position → any, variadic → any[], docstrings say T | null); deprecation_message@deprecated; name collisions hard-fail generation with an actionable error. Structural helpers for object shapes: provider-generator: structural typing for object-shaped provider-function parameters and returns #336.
  3. App-owned usage registries — recording moved to token-resolve time keyed by the resolving stack's node.root (WeakMaps); the App-constructor resets and the "pathological" disclaimer are gone; the interleaved-App false negative is structurally impossible and pinned by regression tests for both registries.
  4. Functions move onto the provider classtime.functions.rfc3339Parse(ts) via a memoized getter fed by this.terraformResourceType (the exact required_providers key, so the namespace is correct by construction); the static class and providerLocalName parameter are gone, mooting the providerAlias naming question. Per-method self-reference JSDoc dropped (kept once on invoke; docs PR: docs(concepts): provider-defined functions + caveats cdk-terrain-docs#29).
  5. CleanupsregisterProviderFeatureUsage moved to TerraformElement (ephemeral resources reuse it); enum casts replaced by a membership guard + direct ProviderFeature indexing; hints get their own map instead of runtime label-mangling (messages byte-identical); write-only registration treats explicit null as omission (!= null, both constructor and setter paths, four pinned tests).
  6. Demo-validation fixes — exercising the generated bindings in a real app (demo PR) caught three bugs snapshots couldn't: the sibling import needed /index under nodenext; the wrapper constructor's TS parameter property is rejected by Node's native type-stripping (jsii accepts it — Node is the stricter consumer); variadic rest params need Array<T> for union element types.
  7. Edge-provider compile coverage (closes Edge-provider schema: cross-language compile coverage for ephemeral resources, provider functions, write-only attributes #311) — the edge schema now carries nine functions (one per generator branch, incl. all three nullability shapes and deprecation), an ephemeral resource, and a write_only attribute; its bindings ride the existing postbuild jsii + pacmak flow for all five languages and the test/<lang>/edge native-compile suites, independent of CI's Terraform version.

External validation (full matrix in the demo PR): terraform 1.15.7 and opentofu 1.12.3 generate byte-identical bindings modulo registry-host doc links; schema cache keys per CLI product+minor; omitted/explicit/mid-position nullable args all render the bare null keyword in position; generated bindings pass tsc + jsii + pacmak; the targetVersions validation error was demonstrated live through the resolve-time registry.

Resolves Terraform CDK issues

🤖 Generated with Claude Code

@sakul-learning

Copy link
Copy Markdown
Contributor

Draft PR #296 review notes / verification results

I built and published a small reusable demo harness against this PR head so the new provider-protocol features can be exercised with real generated AWS bindings:

https://github.com/sakul-learning/cdktn-provider-features-demo

PR head used for the run: 78299b4b9ece16c45ab26a0cd28dec08e8ba57e9.

Verification performed

Using the PR-head packages directly, not npmjs:

  • pnpm run assert:prhead-deps confirms cdktn resolves from the PR-head worktree and the CLI comes from the PR-head cdktn-cli bundle.
  • Terraform 1.15.7:
    • captured AWS provider schema
    • generated AWS bindings
    • ran schema inspection and generated-feature assertions
    • ran pnpm exec tsc --noEmit
    • ran pnpm run synth:all
    • ran pnpm run plan:all
  • OpenTofu 1.12.3:
    • reused the Terraform-generated bindings
    • ran generated-feature assertions
    • ran pnpm exec tsc --noEmit
    • ran pnpm run synth:all
    • ran pnpm run plan:all

The AWS provider schema from Terraform 1.15.7 exposed:

  • provider functions: 4
  • ephemeral resources: 10
  • list resources: 153
  • provider actions: 11
  • resource identities: 437

Generated bindings confirmed:

  • provider functions: yes
  • ephemeral resources: yes
  • list resources: no first-class generated bindings observed
  • provider actions: no first-class generated bindings observed
  • resource identities: no first-class generated bindings observed

OpenTofu 1.12.3 successfully planned the examples with the Terraform-generated bindings. A separate OpenTofu schema probe exposed provider functions, ephemeral resources, and resource identities, but reported list_resource_schemas: 0 and action_schemas: 0, so Terraform 1.15.7 is the better generator binary for this comprehensive AWS capability test.

UX / integration notes from the demo

These are non-blocking, but important feedback from trying to use the generated bindings in a real app:

  • I switched the examples from the AWS barrel import (./.gen/providers/aws) to direct generated-module imports (provider, provider-functions, and the specific ephemeral resource module). The barrel import path pulled in thousands of generated AWS classes and hit the Node/TypeScript OOM path during synth.
  • After that import change, I had to replace the remaining namespace-style references (provider.*, providerFunctions.*, and ephemeral...*) with direct class references like AwsProvider, AwsProviderFunctions, and EphemeralAwsSecretsmanagerRandomPassword.
  • The ephemeral output needs both Fn.ephemeralasnull(...) and sensitive: true so Terraform and OpenTofu accept the plan. Without ephemeralasnull, the ephemeral value is used in a non-ephemeral context; without sensitive, the output still fails because it is derived from secret ephemeral data.
  • I removed a trial use of AwsProviderFunctions.userAgent(...) inside AwsProvider configuration after OpenTofu reported a provider self-reference/cycle. The mistaken assumption was that provider-defined functions behave like pure static helpers that are safe to use while configuring that same provider. The generated call is still a provider-namespaced function (provider::aws::user_agent), so putting it inside the aws provider block asks Terraform/OpenTofu to evaluate an AWS provider function while the AWS provider is still being configured. The demo now exercises provider functions in outputs instead, after provider configuration exists.

Non-blocking findings / suggested follow-ups

  • targetVersions validation bypass in cdktn get path
    targetVersions is validated in parseConfig (packages/@cdktn/commons/src/config.ts), but cdktn get appears to use CdktfConfig.read() plus the raw targetVersions getter (packages/@cdktn/cli-core/src/lib/cdktf-config.ts, then passed through in packages/@cdktn/cli-core/src/lib/get.ts). That path appears to bypass the parseConfig validator, so malformed targetVersions can reach generation.

  • Ephemeral resources expose too much lifecycle surface
    TerraformEphemeralMetaArguments currently exposes lifecycle?: TerraformResourceLifecycle. Terraform ephemeral blocks only support lifecycle precondition / postcondition; they do not support the full managed-resource lifecycle surface such as ignore_changes, replace_triggered_by, create_before_destroy, etc. I’d introduce a narrowed lifecycle type for ephemeral resources, e.g. only precondition and postcondition, and use that in TerraformEphemeralMetaArguments.

  • Nested write-only attributes do not register feature usage
    The constructor-time write-only registration only iterates top-level assignable attributes in resource.configStruct.assignableAttributes. Provider schemas can put write_only inside nested blocks, not only on top-level resource config attributes, so nested write-only usage can skip registerProviderFeatureUsage("writeOnlyAttributes").

  • Generated ephemeral resources skip write-only feature usage registration
    The generator currently limits write-only registration to classes whose parent is TerraformResource; generated ephemeral resources do not get that registration path. Maybe current provider schemas do not combine ephemeral resources with write-only attributes, but since this PR is adding generalized newer-protocol support, I’d either cover the ephemeral path or explicitly document/filter why that combination cannot happen.

Overall: the core provider-function and ephemeral-resource generated bindings worked in real synth/plan flows for Terraform and OpenTofu once the UX issues above were accounted for. The largest current generated-coverage gap is that Terraform exposes AWS list resources, actions, and resource identities, but I did not see first-class generated bindings for those schema sections yet.

so0k added a commit that referenced this pull request Jul 2, 2026
… path

CdktfConfig.targetVersions (used by runGetInDir) returned the raw
cdktf.json value, bypassing the parseConfig validation the main get
handler goes through — and a malformed range then made
semver.intersects throw inside the fetch-time emission check, crashing
cdktn get with a raw stack trace.

Two layers: the getter now validates via commons validateTargetVersions
(warn + ignore invalid targets; generation proceeds, only the warning/
stamp lose them), and checkSchemaEmissionGapFamilies treats an invalid
target range as not-wanted instead of throwing — a best-effort
diagnostics path must never break get.

Found in draft-PR verification (#296 review notes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
so0k added a commit that referenced this pull request Jul 2, 2026
Terraform ephemeral blocks support only precondition/postcondition in
lifecycle — createBeforeDestroy, preventDestroy, ignoreChanges and
replaceTriggeredBy are state-oriented concepts that do not apply to
stateless ephemeral resources. New TerraformEphemeralResourceLifecycle
replaces the full TerraformResourceLifecycle on the ephemeral API;
narrowing later would be jsii-breaking, so it lands before first
release.

Found in draft-PR verification (#296 review notes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
so0k added a commit that referenced this pull request Jul 2, 2026
…generated JSDoc

Calling a provider-defined function inside the configuration block of
the same provider asks Terraform/OpenTofu to evaluate the function
while that provider is still being configured — a self-referential
cycle. Generated method JSDoc and TerraformProviderFunction.invoke now
carry the caveat.

Also documents why write-only usage registration deliberately skips
ephemeral resources: write-only is a state concept, ephemeral resources
have no state, and no provider schema in the RFC-04 sweep combines the
two.

From draft-PR verification (#296 review notes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
so0k added a commit that referenced this pull request Jul 5, 2026
… path

CdktfConfig.targetVersions (used by runGetInDir) returned the raw
cdktf.json value, bypassing the parseConfig validation the main get
handler goes through — and a malformed range then made
semver.intersects throw inside the fetch-time emission check, crashing
cdktn get with a raw stack trace.

Two layers: the getter now validates via commons validateTargetVersions
(warn + ignore invalid targets; generation proceeds, only the warning/
stamp lose them), and checkSchemaEmissionGapFamilies treats an invalid
target range as not-wanted instead of throwing — a best-effort
diagnostics path must never break get.

Found in draft-PR verification (#296 review notes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
so0k added a commit that referenced this pull request Jul 5, 2026
Terraform ephemeral blocks support only precondition/postcondition in
lifecycle — createBeforeDestroy, preventDestroy, ignoreChanges and
replaceTriggeredBy are state-oriented concepts that do not apply to
stateless ephemeral resources. New TerraformEphemeralResourceLifecycle
replaces the full TerraformResourceLifecycle on the ephemeral API;
narrowing later would be jsii-breaking, so it lands before first
release.

Found in draft-PR verification (#296 review notes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
so0k added a commit that referenced this pull request Jul 5, 2026
…generated JSDoc

Calling a provider-defined function inside the configuration block of
the same provider asks Terraform/OpenTofu to evaluate the function
while that provider is still being configured — a self-referential
cycle. Generated method JSDoc and TerraformProviderFunction.invoke now
carry the caveat.

Also documents why write-only usage registration deliberately skips
ephemeral resources: write-only is a state concept, ephemeral resources
have no state, and no provider schema in the RFC-04 sweep combines the
two.

From draft-PR verification (#296 review notes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@so0k
so0k force-pushed the feat/provider-feature-availability branch from ac13f35 to 71148c5 Compare July 5, 2026 12:55
so0k added a commit that referenced this pull request Jul 7, 2026
… path

CdktfConfig.targetVersions (used by runGetInDir) returned the raw
cdktf.json value, bypassing the parseConfig validation the main get
handler goes through — and a malformed range then made
semver.intersects throw inside the fetch-time emission check, crashing
cdktn get with a raw stack trace.

Two layers: the getter now validates via commons validateTargetVersions
(warn + ignore invalid targets; generation proceeds, only the warning/
stamp lose them), and checkSchemaEmissionGapFamilies treats an invalid
target range as not-wanted instead of throwing — a best-effort
diagnostics path must never break get.

Found in draft-PR verification (#296 review notes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
so0k added a commit that referenced this pull request Jul 7, 2026
Terraform ephemeral blocks support only precondition/postcondition in
lifecycle — createBeforeDestroy, preventDestroy, ignoreChanges and
replaceTriggeredBy are state-oriented concepts that do not apply to
stateless ephemeral resources. New TerraformEphemeralResourceLifecycle
replaces the full TerraformResourceLifecycle on the ephemeral API;
narrowing later would be jsii-breaking, so it lands before first
release.

Found in draft-PR verification (#296 review notes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
so0k added a commit that referenced this pull request Jul 7, 2026
…generated JSDoc

Calling a provider-defined function inside the configuration block of
the same provider asks Terraform/OpenTofu to evaluate the function
while that provider is still being configured — a self-referential
cycle. Generated method JSDoc and TerraformProviderFunction.invoke now
carry the caveat.

Also documents why write-only usage registration deliberately skips
ephemeral resources: write-only is a state concept, ephemeral resources
have no state, and no provider schema in the RFC-04 sweep combines the
two.

From draft-PR verification (#296 review notes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@so0k
so0k force-pushed the feat/provider-feature-availability branch from 71148c5 to 244dd68 Compare July 7, 2026 02:56
@so0k
so0k marked this pull request as ready for review July 7, 2026 12:30
@so0k
so0k requested a review from a team as a code owner July 7, 2026 12:30
so0k added a commit that referenced this pull request Jul 7, 2026
Once any provider function was invoked in a long-lived process, later
unrelated apps in the same process (sequential apps in one jest file
being the common case) saw the process-global usedProviderFunctions set
and could fail validation despite never using provider functions — the
new validator registers unconditionally on every stack. The Fn registry
shares the design and the same cross-App leak, but its validator is
feature-flag-gated.

The registries stay process-global (tokens are scope-free; per-stack
attribution is impossible by design) — but App construction now resets
both, scoping usage to the current synthesis session. Within one App
the global registry remains by design: every stack resolves the same
targetVersions from App context, so a sibling-stack validation reports
the same error the using stack would.

Regression tests per review: app1 uses a provider function under
satisfying targets, then app2 with default targets and no usage synths
clean in the same process; analogous flag-gated Fn-registry case.

From PR #296 review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
so0k added a commit that referenced this pull request Jul 7, 2026
…s too

The fetch-time emission-gap warning lived inside readProviderSchema, so
cachedAccess bypassed it entirely on a hit: a schema cached by an old
CLI was reused silently even when the project's targetVersions admit
the newer sections that fetch could never have contained.

The warning moves to readSchema and now runs after every provider
schema resolves — cached or fresh — using the cli_name/cli_version
stamps the schema itself carries (falling back to probing the current
binary when stamps are absent). Regression test drives readSchema twice
against a temp cache dir: one producer call, two warnings.

From PR #296 review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
so0k added a commit that referenced this pull request Jul 7, 2026
providerFeatureConstraints (usage boundaries, packages/cdktn) and
SCHEMA_EMISSION_BOUNDARIES (fetching-CLI emission boundaries,
@cdktn/provider-schema) now name features-matrix.json as their source,
cross-reference each other, and spell out the deliberate OpenTofu
provider-functions exception (language support 1.7.0 vs schema emission
1.8.0). The matrix README gains an in-repo consumers section.

An automated drift check between the maps and the matrix is tracked in
#309. From PR #296 review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/@cdktn/provider-generator/src/get/generator/emitter/resource-emitter.ts Outdated
@vincenthsh

Copy link
Copy Markdown
Contributor

While extending cdktn-provider-features-demo to port real aws-cdk-lib L2 construct logic onto generated awscc bindings + the cfncompat provider's intrinsic-function polyfills, I hit two cdktn-side bugs (not cfncompat bugs) worth flagging here. Full context/examples: examples/l2-kinesis-stream, README "UX notes" section.

1. Provider-function bindings that return a documented "dynamic" type are force-coerced to string, silently dropping struct-typed results

Some provider::*::* functions (e.g. cfncompat's condition_if, matching Fn::If) are documented as returning a dynamic type — either branch of Fn::If can be a string or an object. But the generated TS wrapper unconditionally routes the result through Token.asString(...):

public static conditionIf(condition: any, valueIfTrue: any, valueIfFalse: any, providerLocalName?: string): any {
  return cdktn.Token.asString(cdktn.TerraformProviderFunction.invoke(providerLocalName ?? "cfncompat", "condition_if", [condition, valueIfTrue, valueIfFalse])) as any;
}

That's fine for the common string-return case, but breaks when the result is assigned to a struct-typed resource attribute: Tokenization.isResolvable() doesn't recognize a Token.asString-wrapped value, so the struct's generated setter (e.g. KinesisStreamStreamEncryptionOutputReference.set internalValue) treats it as a plain object with no known keys and the whole attribute silently disappears from synth output — no warning, no error.

Repro:

new KinesisStream(this, "stream", {
  streamEncryption: CfncompatProviderFunctions.conditionIf(
    someCondition, {}, { encryptionType: "KMS", keyId: "alias/aws/kinesis" },
  ),
});

terraform plan shows no stream_encryption key at all in the planned resource.

Workaround: call cdktn.TerraformProviderFunction.invoke("cfncompat", "condition_if", [...]) directly, bypassing the generated wrapper — its return type (IResolvable, no Token.asString) is handled correctly by struct setters.

Suggested fix: only wrap a provider function's result in Token.asString(...) when its schema-declared return type is actually string-shaped; leave functions documented/typed as dynamic (condition_if, find_in_map, select, ...) as a raw IResolvable.

2. TerraformProviderFunction.invoke(...) silently drops literal null positional arguments from any call

TerraformProviderFunction.invoke's runtime validates its whole args array with a single variadic(anyValue) validator — the same one used for genuinely variadic argument lists (e.g. condition_and/condition_or's conditions). variadic's underlying listOf() unconditionally filters out null/undefined entries, which is correct for a variadic list, but wrong for a fixed-arity call: passing null for one positional argument (e.g. condition_if(condition, value_if_true, value_if_false)'s value_if_true) drops it from the argument list entirely instead of preserving its slot.

Repro:

TerraformProviderFunction.invoke("cfncompat", "condition_if", [someCondition, null, { a: 1 }])

terraform plan fails:

Error: Not enough function arguments
while calling provider::cfncompat::condition_if(condition, value_if_true, value_if_false)
Function "provider::cfncompat::condition_if" expects 3 argument(s). Missing value for "value_if_false".

(Terraform actually complains about the third argument going missing, since null — meant for the second slot — got filtered and everything shifted.)

Workaround: avoid passing null; substitute an empty struct ({}) or other schema-appropriate sentinel where possible. Not a great substitute for CloudFormation's Aws.NO_VALUE (property omission) semantics, but the closest expressible result today.

Suggested fix: TerraformProviderFunction.invoke should validate fixed-arity provider functions per-position (preserving null/undefined in their slot) rather than treating the whole args array as one variadic list; the "flatten variadic trailing args" behavior should only apply when a function is genuinely variadic.

so0k added a commit that referenced this pull request Jul 7, 2026
… path

CdktfConfig.targetVersions (used by runGetInDir) returned the raw
cdktf.json value, bypassing the parseConfig validation the main get
handler goes through — and a malformed range then made
semver.intersects throw inside the fetch-time emission check, crashing
cdktn get with a raw stack trace.

Two layers: the getter now validates via commons validateTargetVersions
(warn + ignore invalid targets; generation proceeds, only the warning/
stamp lose them), and checkSchemaEmissionGapFamilies treats an invalid
target range as not-wanted instead of throwing — a best-effort
diagnostics path must never break get.

Found in draft-PR verification (#296 review notes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@so0k so0k left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup - and rebase required

Comment thread packages/@cdktn/provider-generator/src/get/__tests__/generator/write-only.test.ts Outdated
Comment thread packages/@cdktn/provider-generator/src/get/generator/emitter/resource-emitter.ts Outdated
Comment thread packages/cdktn/src/tfExpression.ts
Comment thread packages/cdktn/test/validations.test.ts Outdated
Comment thread packages/cdktn/test/validations.test.ts Outdated
Comment thread packages/cdktn/test/validations.test.ts Outdated
Comment thread packages/cdktn/test/write-only.test.ts Outdated
# Conflicts:
#	packages/@cdktn/provider-schema/package.json
#	pnpm-lock.yaml
so0k added 4 commits July 22, 2026 23:24
Per review on #296: official JSDoc grammar puts only the type expression
inside @param/@returns braces. The generated set/list semantics
("Terraform set; ordering is not guaranteed and duplicate values are
removed.") and cdktn.Token.nullValue() guidance move out of
docstringType/returnDocstringType into prose emitted after the parameter
name (joined with the schema description) and after the @returns braces,
via optional docstringNote/returnDocstringNote model fields. Nested
plainArrayElementType labels are dropped entirely.
…tokens

The resolvable-resolved-object branch of resolveExpressionPart (new on
this branch) serialized map keys into the Terraform expression verbatim,
so a key containing a quote, backslash, control character, ${...} or
%{...} produced an invalid expression or a template sequence. Keys are
now serialized as Terraform quoted-string literals (JSON.stringify plus
Terraform's own $${ / %%{ escapes), with a regression test covering
hostile keys via Token.asAny({...}).

The pre-existing direct-object branches keep their behavior - tracked in
#350. Also rewords review-round narration in provider-functions.test.ts
comments to present-tense statements evaluated against the merge base.
…e() write-only coverage

Addresses review 4731098548 on #296:
- Reword remaining intra-PR narration ("Round N", "anymore", "used
  to") across validations.test.ts, write-only.test.ts,
  terraform-ephemeral-resource.test.ts, resource-emitter.ts and the
  provider-generator tests into present-tense statements that hold
  against the squash-merge base; regression framings now say explicitly
  when they refer to released (merge-base) behavior.
- Drop the not.toMatch(registerProviderFeatureUsage) assertion - it
  tested absence of an internal call from a superseded design.
- Pin that a token RESOLVING to null (cdktn.Token.nullValue()) is
  treated as write-only omission, complementing the literal-null tests.
- Document in _runPreparingResolve why cross-stack references stay
  sound without this stack's own backend injection
  (registerIncomingCrossStackReference materializes the origin stack's
  backend itself).
@so0k

so0k commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Fifth review round: conflict resolution + cleanup review + blocking escaping fix

Pushed a2ea87a4b..f04238e8f (no force-push; the platform squash at merge produces the final single commit).

Conflict resolutionorigin/main merged into the branch (0d1ffd758): main's fs-extra 11.3.4→11.3.6 bump collided with this branch adding semver to @cdktn/provider-schema; resolution keeps both, lockfile regenerated.

@so0k review 4731098548 — all 16 comments addressed (threaded replies on each):

@sakul-learning comment 5016340571:

  • Blocking item fixed in ebc9a25f8: the newly-added resolvable-resolved-object branch now serializes map keys as Terraform quoted-string literals (JSON.stringify + Terraform's own $${/%%{ escapes), with a focused regression test using Token.asAny({...}) and quote/backslash/template-marker keys. Scope is exactly the new path; the pre-existing direct-object branch and HCL renderer remain as-is, tracked in tfExpression/HCL renderer: object keys are rendered into expressions without escaping #350. (Implementation note: the escapes use replacer functions — a replacement string "$${" silently collapses to ${ because $$ is itself a replacement-string escape; the regression test caught this.)
  • JSDoc: implemented per your minimal correction — docstringType/returnDocstringType stay the richer conceptual Terraform types but are now type-only; generated guidance joins the schema description as prose, with one small optional note field for returns.
  • Parity/future-drift: maintainer decision was follow-up issue — filed Guard collection type-mapping parity across generator surfaces (table-driven test + shared-descriptor evaluation) #353 (table-driven parity test + shared-descriptor evaluation).

Verification: provider-generator 105/105; cdktn 541/542 (the one failure is the pre-existing env-only toPlanSuccessfully docker matcher). A final adversarial pass over git diff origin/main...HEAD confirmed no remaining narration, verified the escaping semantics (including keys already containing $${), the note propagation through all model paths, and merge consistency.

…resolve

eslint @typescript-eslint/no-this-alias flagged the 'const resource =
this' capture. The resolve() member becomes an arrow function (lexical
'this' is the resource); toString() stays a shorthand method since its
'this' must be the wrapper token object.
@sakul-learning

Copy link
Copy Markdown
Contributor

Follow-up on two non-blocking review points at 4aa026c5:

Terraform object-key escaping — non-blocking

In packages/cdktn/src/tfExpression.ts, the new quotedStringKey() helper emits quoted Terraform object keys for the resolved whole-collection expression path. It correctly protects quotes, backslashes, Terraform interpolation markers (${...}), and Terraform template directives (%{...}).

One serializer-completeness edge case remains: JavaScript's JSON.stringify() represents U+0008 (backspace) and U+000C (form feed) as \b and \f; Terraform 1.7.5 rejects those escapes in quoted strings. Consequently, an object key containing either literal control character would cause Terraform configuration parsing to fail. Such keys are exceptionally uncommon, the failure is explicit rather than silent, and this edge case does not justify blocking this PR.

The follow-up is tracked in #350. This PR does not need to broaden its scope into a complete Terraform string serializer.

Generated collection documentation — use the compact recursive form

In provider-function-model.ts, generated JSDoc should describe Terraform collection structure recursively, following Jon's suggestion:

  • Terraform list → Array<T>
  • Terraform set → Set<T>

This yields compact descriptions such as Array<Set<string>> for list(set(string)) and Set<Array<string>> for set(list(string)), including deeper finite nesting, without recursively expanding prose. The generated TypeScript/jsii signature remains the source of truth for accepted runtime values; the JSDoc notation communicates Terraform collection semantics. Snapshots covering list(set(string)) and set(list(string)) are sufficient for this PR; arbitrary-depth permutations and nested prose notes are out of scope.

At 4aa026c5, the configured validation suite passed 41/41, the cdktn build passed, and the full cdktn test suite passed 542/542. With the serializer edge case tracked separately and the documentation scope above, neither review point blocks this PR.

…erraform collections

Follows jsteinich's recommendation (relayed in
#296 (comment)):
collection docstring types describe the Terraform structure recursively -
list -> Array<T>, set -> Set<T> - e.g. list(set(string)) documents as
Array<Set<string>>. Lists lose their prose note (the notation is the
default semantics); sets keep the ordering/duplicates note since the jsii
type remains an array and a reader must not literally pass a JS Set.
Fixture + assertions cover list(set(string)) and set(list(string)).
@so0k

so0k commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Re #296 (comment) — both points handled:

  1. Serializer edge case (\b/\f JSON escapes rejected by Terraform quoted strings): agreed non-blocking; noted on tfExpression/HCL renderer: object keys are rendered into expressions without escaping #350, which now owns the complete-serializer follow-up for all object-key paths.
  2. Compact recursive notation: implemented in ba598929e following Jon's suggestion — list → Array<T>, set → Set<T>, recursively (list(set(string))Array<Set<string>>, set(list(string))Set<Array<string>>). Snapshots plus explicit assertions cover exactly those two shapes; deeper nesting composes through the same recursion and prose notes stay top-level-only (sets only), per the stated scope. This also settles the earlier silent-lists question: lists now carry no prose note at all.

@so0k
so0k enabled auto-merge (squash) July 23, 2026 00:45
@sakul-learning

Copy link
Copy Markdown
Contributor

Final verification at ba598929e: I found no release-blocking issue in the provider-function, ephemeral-resource, write-only-attribute, or target-version validation changes in this PR.

The public demo in sakul-learning/cdktn-provider-features-demo#5 was regenerated against this PR head; its resulting AWS, AWSCC, and cfncompat bindings are committed at 3b3468f68.

Verification summary:

  • Regenerated every provider configured by the demo; the generated TypeScript tree typechecked and all 9 examples synthesized.
  • Generated-code and Terraform checks verified that provider-defined functions are invoked through the owning provider instance (provider.functions), so the provider local name is selected by that instance; they also covered nested function composition, positional null, and ephemeral-to-write-only wiring.
  • Collection type rendering preserves Array<T> for lists and Set<T> for sets recursively, while emitted jsii/TypeScript function signatures use arrays and IResolvable where required.
  • Terraform 1.15.7 validated all 9 synthesized configurations, and terraform plan -refresh=false completed for all 9. No apply was run.
  • Configured repository validation checks passed 41/41; the cdktn build succeeded; and cdktn tests passed 542/542.

The rare U+0008/U+000C JSON-escape case is non-blocking and tracked with the broader object-key escaping follow-up in #350.

Verdict: APPROVE.

@so0k
so0k merged commit 90322f9 into main Jul 23, 2026
514 of 516 checks passed
@so0k
so0k deleted the feat/provider-feature-availability branch July 23, 2026 04:19
so0k added a commit that referenced this pull request Jul 23, 2026
#349 ("Upgrade dependencies for all") was branched from an older main
(pre-#345 Node 22, pre-#277 cdktf removal) and now conflicts. Rather than
merge its stale tree, this pulls in its genuine, non-cli upgrades so #349
can be closed as superseded by this PR:

- @cdktn/commons: follow-redirects 1.15.11 -> 1.16.0, semver 7.7.4 -> 7.8.5
- @cdktn/hcl2cdk: @babel/{generator,template,types} -> 7.29.7, tsx 4.6.1 -> 4.23.1
- @cdktn/provider-schema: semver 7.7.4 -> 7.8.5
- typescript devDep floor ^5.0.0 -> ^5.0.4 across commons/hcl2cdk/hcl2json/
  provider-generator/provider-schema

provider-schema's semver was bumped to 7.8.5 for lockfile consistency
(it postdates #349's base via #296, so #349 never saw it).

The cli/cli-core bumps #349 also carried are already in this PR. Verified:
build + unit tests green for commons, hcl2cdk (279 tests / 202 snapshots),
hcl2json, provider-generator, provider-schema, and cdktn-cli.
so0k pushed a commit that referenced this pull request Aug 7, 2026
🤖 Release PR — merge to cut a new release. Kept open and rebased
as commits land on `main`.
---


<details><summary>0.24.0</summary>

##
[0.24.0](v0.23.4...v0.24.0)
(2026-08-06)


### ⚠ BREAKING CHANGES

* **lib:** validate Terraform function versions by default
([#362](#362))
* **deps:** Require Node 22 minimum
([#345](#345))
* **lib:** preserve symlinks in TerraformAsset walkers
([#321](#321))
* **cli:** replace node-fetch with undici
([#306](#306))

### Features

* **lib:** canonical asset hashes behind the canonicalAssetHashes
feature flag
([#323](#323))
([76dd4ff](76dd4ff))
* **lib:** validate Terraform function versions by default
([#362](#362))
([4ac0736](4ac0736))
* support newer provider plugin-protocol features via targetVersions
(RFC-04)
([#296](#296))
([90322f9](90322f9))


### Bug Fixes

* **cli:** don't downgrade prebuilt providers on a transient registry
failure
([#298](#298))
([a960c5c](a960c5c))
* **cli:** include dev dependencies in npm version lookup
([#280](#280))
([955204a](955204a))
* **docs:** fix stale constructs pin breaking with()/IMixin docs
([#305](#305))
([605cf63](605cf63))
* **gha:** Allow pnpm to update the lockfile after package updates
([#318](#318))
([a899b7c](a899b7c))
* **gha:** Fix pnpm upgrade workflow
([#335](#335))
([e1a69fc](e1a69fc))
* **gha:** flip merged release PR label to autorelease: tagged
([#302](#302))
([8d64f6c](8d64f6c))
* **gha:** mint the Go-publish token from the CDKTN Maintainers app
([#369](#369))
([71921ce](71921ce))
* **gha:** mint the Go-publish token from the open-constructs-cdktn App
([#368](#368))
([179f10c](179f10c))
* **gha:** pass the Go-publish App token as x-access-token userinfo
([#370](#370))
([4e3ff19](4e3ff19))
* **lib:** Disallow constructs 10.8 until support can be added
([#363](#363))
([8bdae0d](8bdae0d))
* **lib:** preserve symlinks in TerraformAsset walkers
([#321](#321))
([6360e20](6360e20))
* typo in `moveFromId` JSDoc
([#355](#355))
([e1cf8ce](e1cf8ce))


### Miscellaneous Chores

* **cli:** replace Ink + React with smaller-tree CLI libraries
([#264](#264))
([a6aff7e](a6aff7e))
* **cli:** replace node-fetch with undici
([#306](#306))
([1317141](1317141))
* **deps:** bump glob to 13.0.6
([#307](#307))
([47ee2bb](47ee2bb))
* **deps:** bump the github-actions-backward-compatible group with 2
updates
([#295](#295))
([eab2a01](eab2a01))
* **deps:** replace lerna with nx
([#315](#315))
([94999fc](94999fc))
* **deps:** Require Node 22 minimum
([#345](#345))
([2bf315d](2bf315d))
* **deps:** update ci-info to 4.4.0 across all packages
([#329](#329))
([557a163](557a163))
* **deps:** update fs-extra to 11.3.6 across all packages
([#328](#328))
([95753a9](95753a9))
* **deps:** update minimatch to 10.2.5
([#330](#330))
([d1c1c53](d1c1c53))
* **deps:** Update sscaff to v2.0.388
([#331](#331))
([38ea0ba](38ea0ba))
* **deps:** Update zod to v4.4.3
([#332](#332))
([c10ee35](c10ee35))
* **deps:** Upgrade dependencies
([#347](#347))
([45d3a66](45d3a66))
* remove cdktf from tests
([#277](#277))
([dc9a8e9](dc9a8e9))
* ship Terraform 1.15.8 in the jsii-terraform image
([#367](#367))
([deaa9b0](deaa9b0))
* Upgrade dependencies for lib
([#348](#348))
([d616f17](d616f17))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: open-constructs-cdktn[bot] <291052431+open-constructs-cdktn[bot]@users.noreply.github.com>
so0k added a commit to cdktn-io/cdktn-repository-manager that referenced this pull request Aug 7, 2026
## Summary

- Adds
[`cdktn-io/terraform-provider-cfncompat`](https://github.com/cdktn-io/terraform-provider-cfncompat/releases/tag/v0.1.0)
(`v0.1.0`) to `provider.json` as `cfncompat`
- Adds `cfncompat` to the primary `repos` stack in `sharded-stacks.json`

## Why draft

`cfncompat` exposes its functionality entirely as [17 provider-defined
functions](https://github.com/cdktn-io/terraform-provider-cfncompat/releases/tag/v0.1.0)
(`provider::cfncompat::*`), implementing CloudFormation intrinsic
functions for use in Terraform. CDKTN doesn't yet generate bindings for
provider-defined functions — that support is being added in
[open-constructs/cdk-terrain#296](open-constructs/cdk-terrain#296)
(RFC-04), which is still a draft upstream.

Keeping this PR in draft until open-constructs/cdk-terraim#296 lands and
ships in a CDKTN release, since generating bindings for this provider
today would produce an empty/unusable package (no resources/data
sources, only functions).

## Test plan

- [x] Validated `provider.json` and `sharded-stacks.json` remain valid
JSON
- [x] Verified the `cfncompat` key satisfies the provider name/key
matching validation in `main.ts` (`validateProviderNames`)
- [ ] Re-run `yarn build && yarn synth` once dependencies support
provider-function generation
- [ ] Mark ready for review once open-constructs/cdk-terrain#296 lands


---
_Generated by [Claude
Code](https://claude.ai/code/session_01B46R1TJvB3ib7rjK3sxRZw)_

Co-authored-by: Claude <noreply@anthropic.com>
X-Guardian pushed a commit to X-Guardian/cdk-terrain that referenced this pull request Aug 10, 2026
…ons (RFC-04) (open-constructs#296)

Implements RFC-04 — **provider feature availability**: support the newer
provider plugin-protocol capability families with `targetVersions`-aware
codegen and synth-time validation. Proposal, dataset, sweep tooling and
interactive report live in **open-constructs/cdktn-planning** →
`RFCS/04-provider-feature-availability/` (only the merged matrix is
vendored here, matching the function-availability split).

Builds directly on the open-constructs#269 foundation (`targetVersions` +
`ValidateFeatureTargetSupport`) and the open-constructs#268 usage-registry pattern.

## Guiding decision

**Generate the full surface the schema offers; narrow per project at
synth time via `targetVersions`.** Generation-time filtering would fork
the generated API by project configuration — impossible for prebuilt
providers and hostile to caching. Constraints baked into the validations
are the per-product `>=` ranges from the sweep dataset.

## Review guide (one commit per RFC rollout item)

1. **`chore: vendor provider-feature availability matrix`** — dataset
digest + README only, no behavior change.
2. **`feat(lib): TerraformEphemeralResource`** — new public base class
synthesizing to the top-level `ephemeral` key (JSON + HCL renderer),
refs as `ephemeral.<type>.<id>.<attr>`, no
provisioners/connection/import/move. Constructor registers the
target-version validation unconditionally (new API surface — only fires
on use, no feature flag per RFC). Internal `providerFeatureConstraints`
map sourced from the vendored matrix.
3. **`feat(provider-generator): acquire newer provider-protocol schema
sections`** (Phase 0, a bugfix on its own) — commons types for
`functions` / `ephemeral_resource_schemas` / `resource_identity_schemas`
/ `write_only`; sanitizer walks ephemeral schemas; fetched schemas
stamped with the fetching CLI; **cache key now includes CLI
product+minor** (a schema fetched once with an old CLI no longer poisons
every later generation); fetch-time warning when the fetching binary
structurally cannot emit sections the targets admit.
4. **`feat(provider-generator): generate ephemeral resource bindings`**
— third schema family mirroring the `data_` pipeline
(`EphemeralRandomPassword` in `ephemeral-random-password/`), config
extends `TerraformEphemeralMetaArguments`, no `generateConfigForImport`.
Zero churn in existing snapshots (ephemeral models append after
resources/data sources; class-name dedup is order-dependent).
5. **`feat(provider-generator): generate provider-defined function
bindings`** — `TimeProviderFunctions.rfc3339Parse(ts)` →
`${provider::time::rfc3339_parse(...)}`; namespace defaults to the
registry short name with a `providerLocalName` override (local names
change the namespace, aliases don't). Runtime flows through one public
jsii chokepoint (`TerraformProviderFunction.invoke`) feeding a usage
registry; `TerraformStack` validates usage against `terraform >=1.8.0` /
`opentofu >=1.7.0` — note the deliberate asymmetry: OpenTofu *language*
support (1.7.0) predates its schema *emission* (1.8.0), so generation
and validation use different boundaries.
6. **`feat(provider-generator): deprecate write-only attribute getters,
validate usage`** — providers never persist write-only values (every
read is `null` by protocol contract), so the state-backed getter is a
trap: emitted `@deprecated` now, removal rides the next prebuilt major
(JSII-breaking otherwise). Setting one (setter or constructor config)
registers usage via a new protected
`TerraformResource.registerProviderFeatureUsage` hook — generated code
reaches the validation machinery only by extending base classes.
7. **`feat(cli): thread targetVersions into cdktn get`** —
`GetOptions.targetVersions` → `ConstructsMaker` → `readSchema`, driving
the Phase 0 fetch-time warning; `constraints.json` gains diagnostic
stamps (`targetVersions`, fetching `cli`) without affecting the
`filterAlreadyGenerated` staleness logic (targets don't change codegen
output, so they must not force regeneration).

## Test coverage

- Generator: snapshot tests against **real `terraform providers schema
-json` fragments** from the sweep (random ephemeral, time functions
incl. object returns, vault `*_wo`), plus a synthetic fixture for
variadic/reserved-name mapping branches. Zero churn in pre-existing
snapshots.
- Core: validation matrices mirroring `validations.test.ts`
(admit/exclude per product, hint text, the OpenTofu 1.7.0 asymmetry,
registry resets).
- Schema: emission-gap logic matrix, cache-key suffix behavior,
sanitizer walk over ephemeral schemas; CLI stamps in network snapshots
are stubbed (environment-dependent).

## Known/deferred

- `matchers.test.ts › toPlanSuccessfully` fails in this environment
before and after these changes (downloads the real docker provider;
verified pre-existing via stash on the unmodified tree).
- Deferred per RFC Phase 4: resource identity consumption (schema
plumbing already carried), list resources / actions / state stores
(Terraform-only today; re-evaluate on opentofu/opentofu#3787).
- **Follow-ups tracked as issues**: open-constructs#308 (nested write-only
registration), open-constructs#309 (matrix drift check / in-repo tooling), open-constructs#310
(`hcl2cdk` ephemeral conversion), open-constructs#311 (edge-provider schema
cross-language coverage), open-constructs#312 (ephemeral integration test gated on TF
>= 1.10), open-constructs#313 (pre-existing HCL renderer defects), open-constructs#304 (first-class
`provider_meta`; open-constructs#314 closed as its duplicate), open-constructs#316 (remove the
deprecated write-only getters at the next prebuilt-provider major — the
Phase 3 end state). New this round: open-constructs#336 (structural typing for
object-shaped function params/returns), open-constructs#337 (runtime CI thin slice on
terraform 1.15.x + opentofu 1.12.x — CI's Terraform ceiling is 1.6.5, so
the new schema sections are covered by fixture unit tests, the
edge-provider compile slice below, and the external demo harness until
open-constructs#337 lands).

## Review findings addressed (from the demo-harness verification
comment)

- **Fixed — `targetVersions` validation bypass**:
`CdktfConfig.targetVersions` (the `runGetInDir` path) now validates via
commons `validateTargetVersions` (warn + ignore), and the emission-gap
check treats invalid ranges as not-wanted instead of throwing — a
malformed range previously crashed `cdktn get` via `semver.intersects`.
- **Fixed — ephemeral lifecycle narrowed**: new
`TerraformEphemeralResourceLifecycle` (`precondition`/`postcondition`
only) replaces the full managed-resource lifecycle on the ephemeral API,
before it ships as jsii surface.
- **Fixed (docs) — provider-fn self-reference cycle**: generated
provider-function JSDoc and `TerraformProviderFunction.invoke` now warn
against calling a provider's functions inside that same provider's
configuration block.
- **Documented — ephemeral × write-only**: registration deliberately
skips ephemeral resources — write-only is a state concept, ephemeral
resources have no state, and no schema in the sweep (incl. vault's 16
ephemeral resources) combines the two.
- **Follow-up — nested write-only registration** (open-constructs#308): `write_only`
inside nested blocks gets the deprecated getter but skips usage
registration; deep config scanning deserves its own PR (a miss degrades
to the plan-time error, not silent breakage).
- **Pre-existing / by design**: the AWS barrel-import OOM predates this
PR (lazy-index is the existing mitigation); `Fn.ephemeralasnull` +
`sensitive` on outputs is Terraform semantics (docs candidate); list
resources / actions / resource identity codegen is the RFC Phase 4
deferral noted above.

## Second review round addressed

- **Fixed — cross-App usage-registry leak**: `App` construction now
resets both usage registries (a new App = a new synthesis session).
Within an App the process-global registry is by design — every stack
resolves the same `targetVersions` from App context — but usage no
longer leaks into later, unrelated Apps in the same process (the
reviewer's jest scenario). Regression tests cover the exact sequence;
the flag-gated `Fn` registry had the same latent leak and is reset too.
- **Fixed — emission-gap warning on cache hits**: the warning moved from
`readProviderSchema` into `readSchema`, running for cached *and* fresh
schemas using the `cli_name`/`cli_version` stamps the schema carries.
Regression test: two `readSchema` calls against a cache dir → one
producer call, two warnings.
- **Docs — matrix as source of truth**: both hand-maintained maps now
name `features-matrix.json` as their source and cross-reference each
other; automated drift check tracked in open-constructs#309.

## Third review round addressed (cfncompat/awscc demo findings)

- **Fixed — dynamic-typed provider-function returns coerced to string**:
generated wrappers for `dynamic`/`object`/`map` return types no longer
wrap in `Token.asString(...)`; they return the raw `invoke()`
`IResolvable`, which `Tokenization.isResolvable()` recognizes —
struct-typed attribute assignments (`OutputReference.internalValue`) no
longer vanish silently from synth output.
- **Fixed — `invoke()` dropped literal `null` positional arguments**:
arguments are now validated per position (variadic flattening stays in
the generated wrappers where the signature is known), and a latent third
bug this exposed was fixed too: `FunctionCall` rendered args via
`Array.prototype.join`, which collapses `null` entries to empty strings
— resolved `null`/`undefined` now render as the Terraform `null`
keyword. Regression tests pin the exact
`provider::cfncompat::condition_if(true, null, {"a" = 1})` rendering.


## Fourth review round addressed (jsteinich's CHANGES_REQUESTED)

Seven commits on top of the reviewed head, one per finding group (inline
replies on each thread carry the details):

1. **Schema fidelity** — `FunctionParameter` gains `is_nullable`,
`FunctionSignature` gains `deprecation_message`, and both drop
`description_kind` (verified against
`internal/command/jsonfunction/{function,parameter}.go` and the OpenTofu
equivalent: that field does not exist on function JSON;
`allow_null_value`/`allow_unknown_values` are protocol-only names — the
old comment was wrong). OpenTofu emits no `deprecation_message` at all
(divergence, documented).
2. **Recursive, honest type mapping** — collections recurse
(`list(number)` → `number[]` via `Token.asNumberList`, `set(bool)` param
→ `Array<boolean | IResolvable>`, nested compose); object/map/dynamic
returns are declared `IResolvable` instead of `any` (property access on
a token is now a compile error); `is_nullable` consumed (trailing →
jsii-optional, mid-position → `any`, variadic → `any[]`, docstrings say
`T | null`); `deprecation_message` → `@deprecated`; name collisions
hard-fail generation with an actionable error. Structural helpers for
object shapes: open-constructs#336.
3. **App-owned usage registries** — recording moved to token-resolve
time keyed by the resolving stack's `node.root` (WeakMaps); the
App-constructor resets and the "pathological" disclaimer are gone; the
interleaved-App false negative is structurally impossible and pinned by
regression tests for both registries.
4. **Functions move onto the provider class** —
`time.functions.rfc3339Parse(ts)` via a memoized getter fed by
`this.terraformResourceType` (the exact `required_providers` key, so the
namespace is correct by construction); the static class and
`providerLocalName` parameter are gone, mooting the `providerAlias`
naming question. Per-method self-reference JSDoc dropped (kept once on
`invoke`; docs PR: open-constructs/cdk-terrain-docs#29).
5. **Cleanups** — `registerProviderFeatureUsage` moved to
`TerraformElement` (ephemeral resources reuse it); enum casts replaced
by a membership guard + direct `ProviderFeature` indexing; hints get
their own map instead of runtime label-mangling (messages
byte-identical); write-only registration treats explicit `null` as
omission (`!= null`, both constructor and setter paths, four pinned
tests).
6. **Demo-validation fixes** — exercising the generated bindings in a
real app ([demo
PR](sakul-learning/cdktn-provider-features-demo#5))
caught three bugs snapshots couldn't: the sibling import needed `/index`
under `nodenext`; the wrapper constructor's TS parameter property is
rejected by Node's native type-stripping (jsii accepts it — Node is the
stricter consumer); variadic rest params need `Array<T>` for union
element types.
7. **Edge-provider compile coverage** (closes open-constructs#311) — the edge schema
now carries nine functions (one per generator branch, incl. all three
nullability shapes and deprecation), an ephemeral resource, and a
`write_only` attribute; its bindings ride the existing postbuild jsii +
pacmak flow for all five languages and the `test/<lang>/edge`
native-compile suites, independent of CI's Terraform version.

**External validation** (full matrix in the [demo
PR](sakul-learning/cdktn-provider-features-demo#5)):
terraform 1.15.7 and opentofu 1.12.3 generate byte-identical bindings
modulo registry-host doc links; schema cache keys per CLI product+minor;
omitted/explicit/mid-position nullable args all render the bare `null`
keyword in position; generated bindings pass tsc + jsii + pacmak; the
targetVersions validation error was demonstrated live through the
resolve-time registry.

## Resolves Terraform CDK issues

- hashicorp/terraform-cdk#3886
- hashicorp/terraform-cdk#3686

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
X-Guardian pushed a commit to X-Guardian/cdk-terrain that referenced this pull request Aug 10, 2026
🤖 Release PR — merge to cut a new release. Kept open and rebased
as commits land on `main`.
---


<details><summary>0.24.0</summary>

##
[0.24.0](open-constructs/cdk-terrain@v0.23.4...v0.24.0)
(2026-08-06)


### ⚠ BREAKING CHANGES

* **lib:** validate Terraform function versions by default
([open-constructs#362](open-constructs#362))
* **deps:** Require Node 22 minimum
([open-constructs#345](open-constructs#345))
* **lib:** preserve symlinks in TerraformAsset walkers
([open-constructs#321](open-constructs#321))
* **cli:** replace node-fetch with undici
([open-constructs#306](open-constructs#306))

### Features

* **lib:** canonical asset hashes behind the canonicalAssetHashes
feature flag
([open-constructs#323](open-constructs#323))
([76dd4ff](open-constructs@76dd4ff))
* **lib:** validate Terraform function versions by default
([open-constructs#362](open-constructs#362))
([4ac0736](open-constructs@4ac0736))
* support newer provider plugin-protocol features via targetVersions
(RFC-04)
([open-constructs#296](open-constructs#296))
([90322f9](open-constructs@90322f9))


### Bug Fixes

* **cli:** don't downgrade prebuilt providers on a transient registry
failure
([open-constructs#298](open-constructs#298))
([a960c5c](open-constructs@a960c5c))
* **cli:** include dev dependencies in npm version lookup
([open-constructs#280](open-constructs#280))
([955204a](open-constructs@955204a))
* **docs:** fix stale constructs pin breaking with()/IMixin docs
([open-constructs#305](open-constructs#305))
([605cf63](open-constructs@605cf63))
* **gha:** Allow pnpm to update the lockfile after package updates
([open-constructs#318](open-constructs#318))
([a899b7c](open-constructs@a899b7c))
* **gha:** Fix pnpm upgrade workflow
([open-constructs#335](open-constructs#335))
([e1a69fc](open-constructs@e1a69fc))
* **gha:** flip merged release PR label to autorelease: tagged
([open-constructs#302](open-constructs#302))
([8d64f6c](open-constructs@8d64f6c))
* **gha:** mint the Go-publish token from the CDKTN Maintainers app
([open-constructs#369](open-constructs#369))
([71921ce](open-constructs@71921ce))
* **gha:** mint the Go-publish token from the open-constructs-cdktn App
([open-constructs#368](open-constructs#368))
([179f10c](open-constructs@179f10c))
* **gha:** pass the Go-publish App token as x-access-token userinfo
([open-constructs#370](open-constructs#370))
([4e3ff19](open-constructs@4e3ff19))
* **lib:** Disallow constructs 10.8 until support can be added
([open-constructs#363](open-constructs#363))
([8bdae0d](open-constructs@8bdae0d))
* **lib:** preserve symlinks in TerraformAsset walkers
([open-constructs#321](open-constructs#321))
([6360e20](open-constructs@6360e20))
* typo in `moveFromId` JSDoc
([open-constructs#355](open-constructs#355))
([e1cf8ce](open-constructs@e1cf8ce))


### Miscellaneous Chores

* **cli:** replace Ink + React with smaller-tree CLI libraries
([open-constructs#264](open-constructs#264))
([a6aff7e](open-constructs@a6aff7e))
* **cli:** replace node-fetch with undici
([open-constructs#306](open-constructs#306))
([1317141](open-constructs@1317141))
* **deps:** bump glob to 13.0.6
([open-constructs#307](open-constructs#307))
([47ee2bb](open-constructs@47ee2bb))
* **deps:** bump the github-actions-backward-compatible group with 2
updates
([open-constructs#295](open-constructs#295))
([eab2a01](open-constructs@eab2a01))
* **deps:** replace lerna with nx
([open-constructs#315](open-constructs#315))
([94999fc](open-constructs@94999fc))
* **deps:** Require Node 22 minimum
([open-constructs#345](open-constructs#345))
([2bf315d](open-constructs@2bf315d))
* **deps:** update ci-info to 4.4.0 across all packages
([open-constructs#329](open-constructs#329))
([557a163](open-constructs@557a163))
* **deps:** update fs-extra to 11.3.6 across all packages
([open-constructs#328](open-constructs#328))
([95753a9](open-constructs@95753a9))
* **deps:** update minimatch to 10.2.5
([open-constructs#330](open-constructs#330))
([d1c1c53](open-constructs@d1c1c53))
* **deps:** Update sscaff to v2.0.388
([open-constructs#331](open-constructs#331))
([38ea0ba](open-constructs@38ea0ba))
* **deps:** Update zod to v4.4.3
([open-constructs#332](open-constructs#332))
([c10ee35](open-constructs@c10ee35))
* **deps:** Upgrade dependencies
([open-constructs#347](open-constructs#347))
([45d3a66](open-constructs@45d3a66))
* remove cdktf from tests
([open-constructs#277](open-constructs#277))
([dc9a8e9](open-constructs@dc9a8e9))
* ship Terraform 1.15.8 in the jsii-terraform image
([open-constructs#367](open-constructs#367))
([deaa9b0](open-constructs@deaa9b0))
* Upgrade dependencies for lib
([open-constructs#348](open-constructs#348))
([d616f17](open-constructs@d616f17))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: open-constructs-cdktn[bot] <291052431+open-constructs-cdktn[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Edge-provider schema: cross-language compile coverage for ephemeral resources, provider functions, write-only attributes

5 participants